docs(recurring): record the invariants and what they cost
Two rules that only review catches, so they go in AGENTS.md §1: occurrences are computed and never stored, and a confirmation's date and its slot are different facts. Collapsing the second pair into one field would silently delete the phone-bill behaviour, which is why it is written down rather than left to the types. §2 gets the termination guard. n >= 1 is rejected at both write boundaries rather than clamped, and the walk throws rather than breaking, because a bill silently dropped from a budget is worse than a visible error. Demo data seeds a monthly rent and a 28-day subscription — the second is what puts two occurrences in one month, which is the case worth looking at. It also seeds a biweekly cost and a lastCharge-anchored one, so every recurrence kind and both anchorings show up in Settings and the month view. DECISIONS.md, PRODUCT.md, ARCHITECTURE.md and TODO.md are updated to match: schema is at 7, the month view carries a second Projected balance beside Remaining, and TODO.md records what a week of real use — not a test — will have to answer.
This commit is contained in:
1 parent
824e94a9bd
commit
0d2aa519b7
6 files changed
+149
-7
No files matched your search
@@ -41,6 +41,58 @@ actions.setRuleFrom(d, transport.id, "2026-01", { kind: "fixed", amount: { amoun
|
||||
actions.setRuleFrom(d, savings.id, "2026-03", { kind: "percentOfIncome", percent: 10 });
|
||||
actions.setRuleOverride(d, "2026-09", social.id, { kind: "fixed", amount: { amount: 2400, currency: "DKK" } });
|
||||
|
||||
// Recurring costs: bills that project their own expected charges, shown as a
|
||||
// second balance ("Projected") beside the allocation-rule figures above. All
|
||||
// three recurrence kinds and both anchorings appear, so the screenshots show
|
||||
// every combination at once.
|
||||
const wowSub = actions.addRecurringCost(d, {
|
||||
name: "World of Warcraft",
|
||||
archived: false,
|
||||
amount: { amount: 13, currency: "USD" },
|
||||
startDate: "2026-06-05",
|
||||
recurrence: { kind: "everyNDays", n: 28 },
|
||||
anchoring: "calendar",
|
||||
splitMode: "percent",
|
||||
splits: [{ postId: games.id, value: 100, absorbsRemainder: true }],
|
||||
});
|
||||
actions.addRecurringCost(d, {
|
||||
name: "Rent",
|
||||
archived: false,
|
||||
amount: { amount: 8200, currency: "DKK" },
|
||||
startDate: "2026-06",
|
||||
recurrence: { kind: "everyNMonths", n: 1 },
|
||||
anchoring: "calendar",
|
||||
splitMode: "percent",
|
||||
splits: [{ postId: rent.id, value: 100, absorbsRemainder: true }],
|
||||
});
|
||||
actions.addRecurringCost(d, {
|
||||
name: "Veg box",
|
||||
archived: false,
|
||||
amount: { amount: 175, currency: "DKK" },
|
||||
startDate: "2026-06-06",
|
||||
recurrence: { kind: "everyNWeeks", n: 2, weekday: 6 },
|
||||
anchoring: "calendar",
|
||||
splitMode: "percent",
|
||||
splits: [{ postId: food.id, value: 100, absorbsRemainder: true }],
|
||||
});
|
||||
// Rebases on the actual charge date rather than the calendar, so an
|
||||
// off-schedule payment moves the whole series — the phone-bill behaviour.
|
||||
actions.addRecurringCost(d, {
|
||||
name: "Car insurance",
|
||||
archived: false,
|
||||
amount: { amount: 349, currency: "DKK" },
|
||||
startDate: "2026-08-14",
|
||||
recurrence: { kind: "everyNDays", n: 30 },
|
||||
anchoring: "lastCharge",
|
||||
splitMode: "percent",
|
||||
splits: [{ postId: transport.id, value: 100, absorbsRemainder: true }],
|
||||
});
|
||||
|
||||
// One confirmed occurrence, so a screenshot shows both states at once: the
|
||||
// expected band lists what is still due, and the fold's two tracks have
|
||||
// already reconverged for this one.
|
||||
actions.confirmOccurrence(d, wowSub.id, "2026-06-05", { date: "2026-06-05" });
|
||||
|
||||
const buy = (date: string, description: string, amount: number, currency: "DKK" | "EUR" | "USD", postId: string, note?: string) =>
|
||||
actions.addPurchase(d, {
|
||||
date, description, note,
|
||||
|
||||
Reference in new issue
Block a user