feat(types): occurrenceDate is optional, stepFrom takes anchoring

A lastCharge cost has no grid of slots, so a purchase against one settles
nothing — its own date is the whole story. occurrenceDate stays required for
calendar, where a slot is exactly what a payment settles.

stepFrom needs the anchoring because everyNMonths means two different things:
a month-granular grid under calendar, a day-precise clamped step under
lastCharge. The lastCharge branch only takes the day-precise path when its
input actually carries a day (date !== monthOf(date)) — a still-unconfirmed
or paid-on-schedule everyNMonths cost has no day to be precise about yet
(its startDate is documented as month-only), and without this guard the
existing walk crashes calling addMonthsToDate on a bare month. This also
means an off-schedule confirmation that rebases the series now correctly
stays day-precise afterward, which updates one pre-existing occurrencesOf
expectation (I3's "paying after the month" case) to match.

Three call sites read source.occurrenceDate as definite and are rewritten
properly in later tasks (3, 5, 6); each gets a narrow scaffolding guard
with a `// rewritten in Task N` comment rather than a cast or `!`.
This commit is contained in:
Lucas Winther committed 2026-09-05 01:03:57 +02:00
1 parent 9035feed5f
commit f8591fdc57
5 files changed
+91 -27

No files matched your search

+6 -1
View File
@@ -428,7 +428,12 @@ export function parseDatasetJson(text: string): Dataset {
`${label} names unknown recurring cost "${purchase.source.recurringId}"`,
);
}
if (!PURCHASE_DATE.test(purchase.source.occurrenceDate)) {
// occurrenceDate is absent for a lastCharge purchase — Task 6 rewrites
// this to check that against the cost's own anchoring.
if (
purchase.source.occurrenceDate !== undefined && // rewritten in Task 6
!PURCHASE_DATE.test(purchase.source.occurrenceDate)
) {
throw new ImportValidationError(
`${label} has an invalid occurrence date "${purchase.source.occurrenceDate}"`,
);