Assignment
NAME
App::Moneymoor::Gateway::Assignment - SQL gateway for the money you give categories each budget period.
SYNOPSIS
use App::Moneymoor::Gateway::Assignment;
# The scheme decides which keys are legal; it defaults to the
# calendar month, whose starts are always 'YYYY-MM-01'.
my $gw = App::Moneymoor::Gateway::Assignment.new(:$db);
$gw.set('2026-03-01', $groceries, 40000); # insert
$gw.set('2026-03-01', $groceries, 45000); # update ā one row/pair
say $gw.get('2026-03-01', $groceries).amount; # 45000
# Budgeting December in March is legal and deliberate:
$gw.set('2026-12-01', $council-tax, 30000);
# Robbing Peter to pay Paul, atomically:
$gw.move-money('2026-03-01', $groceries, $dining, 5000);
say $gw.get('2026-03-01', $groceries).amount; # 40000
say $gw.get('2026-03-01', $dining).amount; # 5000
my @march = $gw.find-by-period('2026-03-01');
$gw.delete('2026-03-01', $dining); # same as setting 0, but
# leaves no row behind
# A pay-day scheme moves the legal keys, and nothing else:
my $pay = App::Moneymoor::Gateway::Assignment.new(
:$db, scheme => App::Moneymoor::Util::Period.monthly(anchor-day => 14));
$pay.set('2026-03-14', $groceries, 40000); # fine
$pay.set('2026-03-01', $groceries, 40000); # Failure: not a start
DESCRIPTION
An assignment is a set, not an increment: there is one row per
(period, category), guaranteed by a unique index, and set
upserts. That is what makes "assign £400 to Groceries in March"
idempotent no matter how many times a UI fires it.
move-money is the other primitive ā two upserts in one run-txn,
so the pair can never half-apply and invent or destroy money. It
adjusts relatively (subtract from one, add to the other), because
that is what moving money means when both categories already have
assignments.
Negative amounts are legal. Pulling £50 back out of a category that
had nothing assigned this period leaves -5000 assigned, and the
engine reads that exactly as written: the category has £50 less, Ready
to Assign has £50 more.
Assigning to the rta category is refused. Ready to Assign is the
pool assignments come from; letting money be assigned to it
would make rule 4 circular.
PERIODS ARE VALIDATED AS STARTS
Every method that takes a period key checks two things and fails
unless both hold: the key is a well-formed YYYY-MM-DD date, and
it is the start of a period under this gateway's scheme
($scheme.period-of($period) eq $period).
The first check is the one the old YYYY-MM validation did, and for
the same reason: a typo'd key does not merely misplace the money, it
extends the derived range, so '20226-03-01' would ask the engine to
derive eighteen thousand years of budget.
The second is stronger, and matters more. A key that is well formed
but is not a start ā '2026-03-15' under the calendar month, or a
'2026-03' row from before the period migration ā creates a
phantom bucket. Nothing rejects it downstream; the derivation
happily sums it alongside the real periods, so the money is neither
lost nor visible, and the budget stops adding up in a way no screen
explains. Storing such a key is worse than storing a typo, because a
typo announces itself.
ATTRIBUTES
dbā requiredApp::Moneymoor::DB.schemeā theApp::Moneymoor::Util::Periodwhose starts are legal keys. Defaults tomonthly/1, the calendar month, which is what every budget written so far uses.
METHODS
set(Str:D $period, Int:D $category-id, Int:D $amount --Model::Assignment)>adjust(Str:D $period, Int:D $category-id, Int:D $delta --Model::Assignment)> ā relative change; the primitivemove-moneyis built from.get(Str:D $period, Int:D $category-id --Model::Assignment)> ā type object when nothing has been assigned.amount-for(Str:D $period, Int:D $category-id --Int)> ā 0 when nothing has been assigned.find-all(--Array)> ā ordered(period_start, id).find-by-period(Str:D $period --Array)>find-by-category(Int:D $category-id --Array)>move-money(Str:D $period, Int:D $from-id, Int:D $to-id, Int:D $amount)delete(Str:D $period, Int:D $category-id)