Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

331 KiB
Raw Blame History

Scratchpad — Service-Period-First Billing and Explicit Cadence Ownership

  • Plan slug: service-period-first-billing-and-cadence-ownership
  • Created: 2026-03-16
  • Replanned: 2026-03-17

What This Is

Keep a lightweight, continuously-updated log of discoveries, sequencing decisions, and subsystem impact notes for the service-period-first billing work.

This scratchpad was expanded on 2026-03-17 after concluding that the first draft plan was materially under-specified for the risk level of the change.

Decisions

  • (2026-03-18) Treat F231 as the logical record-contract checkpoint for materialized recurring periods, not the physical schema checkpoint.
    • Rationale: F232 still needs freedom to choose concrete table/index layout, but the repo needs one shared record vocabulary now so docs, APIs, fixtures, and future migrations do not drift.
  • (2026-03-16) Sequence this plan so service periods become canonical under existing client-cadence behavior before exposing contract-owned cadence. Parity first, new option second, cleanup last.
  • (2026-03-16) Scope the first cut to recurring contract-backed charges. Time and usage remain on their current event-driven model unless a compatibility blocker appears.
  • (2026-03-16) Treat invoice windows as the grouping layer and service periods as the recurring-billing truth for this plan.
  • (2026-03-17) Replan this effort at system breadth rather than billing-engine breadth. The plan must explicitly cover invoice generation, invoice detail consumers, credits/prepayment/negative invoice behavior, APIs/models/repos, templates/wizards/forms, portal/report/export surfaces, migrations/defaulting, and post-cutover cleanup.
  • (2026-03-17) Materialized service periods are now in-scope for v1. The main reason is product editability: if users need to change a future recurring period explicitly, a derived-only model pushes the system toward hidden overrides instead of a first-class editable billing object.
  • (2026-03-17) Use recursive top-down decomposition for the feature/test lists:
    • architecture + parity
    • shared domain
    • client cadence
    • due-position and partial-period rules
    • fixed recurring
    • dependent recurring behaviors
    • invoice generation + billing flows
    • data model/API/UI
    • contract cadence
    • migration/cleanup
  • (2026-03-17) Second-pass agent critique showed the plan still needed dedicated categories for:
    • recurring execution identity and scheduler payloads
    • invoice grouping and split legality
    • parent-charge versus detail-row read-model contracts
    • manual/prepayment/credit service-period policy
    • export adapter flattening rules
    • authoring-path propagation through templates, presets, and custom lines
    • stale dropped-table and repository normalization cleanup

Discoveries / Constraints

  • (2026-03-18) Mixed-cadence coexistence now has one DB-backed export-plus-portal sanity seam, which closes T278, and the remaining conditional split/merge checklist item is now resolved explicitly:

    • server/src/test/integration/accounting/invoiceSelection.integration.test.ts now seeds one historical flat invoice plus one canonical mixed-cadence invoice, then proves AccountingExportInvoiceSelector.createBatchFromFilters(...) stores stable historical-vs-canonical export lines while Invoice.getFullInvoiceById(...) rereads the same canonical recurring detail rows without collapsing the historical invoice into fake recurring metadata
    • the portal-side assertion intentionally uses date-only equality for parent service_period_* summary bounds because DB-backed Date hydration can surface offset-normalized ISO timestamps while the authoritative recurring_detail_periods[] contract remains unchanged; this keeps the test locked to the stable business period semantics instead of one driver-specific timestamp shape
    • T297 is now treated as satisfied by the already-implemented v1 non-support decision in T347: split and merge remain explicitly unsupported edit operations, so the conditional “if supported in v1” behavior never becomes active in this rollout and does not require a contradictory execution path
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/accounting/invoiceSelection.integration.test.ts -t "T278" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Additional DB-backed persisted-ledger lifecycle coverage now closes T323, T324, T325, T326, and T327:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now lets the existing staged-rollout lifecycle seam explicitly claim the regeneration/change-management contract as T316/T323/T324/T327: regenerated future rows supersede only the eligible untouched future slot, edited rows stay preserved, billed rows fail future edit attempts with immutable_record, and the linked invoice_charge_details.item_detail_id row remains traceable from the persisted billed record
    • the same file now adds a dedicated T325 contract-cadence operational-view case, proving future 8th-anchored contract-owned periods can be listed and boundary-edited before invoice generation with the same explicit provenance and exception-state semantics as client cadence
    • the new T326 integration keeps mixed materialization honest at the physical ledger seam instead of only the shared-domain seam: client-owned and contract-owned recurring obligations on the same client persist distinct schedule_key ledgers, retain their own cadence-owner metadata, and keep their own invoice-window anchors (1st vs 8th) without row collisions
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T316|T323|T324|T327|T325|T326" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) DB-backed persisted-ledger inspection/edit/runtime coverage now closes T301, T320, T321, T322, and T328, and exposed a real zero-dollar suppression bug on the live invoice path:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now proves billing staff can list future client-cadence persisted periods, edit a future row with explicit edited provenance, move due selection to a new invoice window without rewriting billed history, and skip the current due row while later persisted work remains selectable
    • the integration fixtures had to align with two live contracts the earlier domain tests did not exercise directly:
      • DB-backed recurring_service_periods date columns hydrate as Date objects, so the integration loader now normalizes service/invoice/activity ranges back to plain YYYY-MM-DD strings before feeding listing/edit/due-selection helpers
      • live runtime selection keys persisted recurring obligations by the effective contract_line_id value exposed as client_contract_line_id in BillingEngine, so the runtime-facing DB tests now materialize obligation ids against fixedLine.contractLineId instead of the helpers separate client-line alias
    • shared/billingClients/recurringServicePeriodEditRequests.ts now accepts an optional recordIdFactory, which closes the remaining wrapper seam between the domain-level edit contract and the physical recurring_service_periods.record_id uuid schema
    • packages/billing/src/actions/invoiceGeneration.ts now honors the real suppress_zero_dollar_invoices boolean instead of checking for an impossible zero_dollar_invoice_handling === 'suppress' enum value; server/src/test/unit/billing/invoiceGeneration.emptyResult.test.ts now locks the real schema contract instead of the impossible enum
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/unit/billing/invoiceGeneration.emptyResult.test.ts src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts src/test/integration/billingInvoiceTiming.integration.test.ts -t "T320|T301|T321|T322|T328|T295" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) DB-backed contract-cadence execution now explicitly covers both monthly and annual selector-window hydration, which closes T276 and T277:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now adds a dedicated monthly T276 case that goes beyond the earlier selector-input sanity check: an 8th-anchored contract-cadence line now proves one contract-owned execution window yields one persisted invoice window and rereads through Invoice.getFullInvoiceById(...) with canonical recurring detail hydration intact
    • the same integration file now adds T277 for annual contract cadence, proving the same selector-input execution identity and detail-backed invoice hydration work when the contract-owned window spans 2025-03-08 through 2026-03-08 instead of a monthly boundary
    • while landing those tests, the only correction needed was in the assertions, not runtime code: Invoice.getFullInvoiceById(...) does not expose invoice-header billing-window fields directly, so the test now checks header persistence via the existing DB reader and reserves the hydrated invoice view assertions for canonical recurring detail projection
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T276|T277" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Report-output parity now explicitly covers the client-cadence no-drift case, which closes T269:

    • server/src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts now runs getContractRevenueReport() against two equivalent client-cadence revenue fact shapes: one legacy invoice-date-only row and one canonical detail-backed row with the same within-year commercial outcome
    • the new T269 assertion matters because the existing report tests mostly covered intended semantic pivots at year boundaries; this one locks the parity requirement that detail-aware readers must not change report output when invoice-date fallback and canonical service-period interpretation are equivalent for the same client-cadence billing result
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts --coverage.enabled false
  • (2026-03-18) Explicit edit-request save/validation responses now cover the missing persisted-period edit wrapper seam, which close T295 and T296:

    • server/src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts now exercises applyRecurringServicePeriodEditRequest(...) directly instead of only the lower-level edit helpers, which is the first test seam that looks like the later UI/API edit surface in F251
    • the new T295 case proves a boundary-adjustment request can succeed with an explicit edited revision and can also fail with structured continuity_overlap_before validation output when sibling context would make the edit invalid
    • the new T296 case proves skip requests return explicit superseding state while billed, invoice-linked rows fail fast through the wrapper with immutable_record instead of pretending defer/repair flows may mutate linked history in place
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts --coverage.enabled false
  • (2026-03-18) Mixed-cadence export-adapter service-period behavior is now explicit, which closes T274:

    • server/src/test/unit/accounting/quickBooksOnlineAdapter.spec.ts now adds a focused mixed-cadence invoice case proving QuickBooks preserves each exported lines own service date even when the same invoice contains both client- and contract-cadence canonical recurring rows
    • server/src/test/unit/accounting/xeroAdapter.spec.ts now adds the corresponding Xero case, proving line-level servicePeriodStart / servicePeriodEnd values remain distinct per recurring line instead of flattening mixed cadence work into one shared range
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/accounting/quickBooksOnlineAdapter.spec.ts src/test/unit/accounting/xeroAdapter.spec.ts --coverage.enabled false
  • (2026-03-18) Portal invoice-detail omission/rendering policy and invoice adapter stability now have explicit checklist coverage, which close T267 and T268:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx now adds a dedicated T267 assertion that the portal dialog intentionally renders canonical detail periods for recurring lines while omitting service-period UI for financial-only/manual rows and showing the explicit fallback copy instead
    • packages/billing/src/lib/adapters/invoiceAdapters.test.ts now tags the existing canonical-detail preservation case as T268, locking the renderer-adapter contract that detail-backed invoice charges still map to one compatibility summary range plus the preserved detail list for downstream preview/rendering consumers
    • focused validation for this checkpoint used:
      • cd packages/client-portal && npx vitest run src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx --coverage.enabled false
      • cd server && npx vitest run ../packages/billing/src/lib/adapters/invoiceAdapters.test.ts --coverage.enabled false
  • (2026-03-18) Invoice round-trip and adjacent reader/action seams now explicitly preserve canonical recurring detail periods, which close T264, T265, and T266:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now proves the full round-trip the earlier DB suites implied but did not lock directly: generateInvoice(...) persists canonical invoice_charge_details, returns an invoice view with recurring_detail_periods, and Invoice.getFullInvoiceById(...) rereads the same canonical detail projection without degrading back to header-only timing
    • server/src/test/unit/billing/invoiceQueries.recurringDetailRead.test.ts now locks the dashboard/query action seam directly by asserting getInvoiceLineItems(...) passes through detail-backed charge rows unchanged, including recurring_projection and recurring_detail_periods
    • server/src/test/unit/billing/manualInvoiceActions.viewing.test.ts now makes the mixed-manual/recurring coexistence surface explicit: manual invoice updates can reread an invoice that still contains canonical recurring detail-backed charges, and those recurring detail rows survive while the manual row stays periodless
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/invoiceQueries.recurringDetailRead.test.ts src/test/unit/billing/manualInvoiceActions.viewing.test.ts --coverage.enabled false
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t \"T264\" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Mixed-cadence grouping now explicitly defends the single-contract split invariant, which closes T261:

    • server/src/test/unit/billing/recurringTiming.domain.test.ts now adds the missing mixed-cadence grouping assertion directly at the shared domain seam: a client-cadence selection and a contract-cadence selection may share the same invoice-window identity, but they still split into separate invoice candidates when clientContractId differs
    • the new test intentionally locks the policy boundary already implied by T141 and T188: cadence owner remains explainability metadata inside each grouped candidate, while single_contract remains the actual split reason when coincident due work spans different contracts
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
  • (2026-03-18) Purchase-order and financial grouping constraints now have their remaining explicit unit guards, which close T262 and T263:

    • server/src/test/unit/billing/recurringTiming.domain.test.ts now adds the narrow policy-specific cases that the earlier generic grouping tests did not spell out: a PO-required line and a non-PO line split inside the same invoice window under purchase_order_scope, and same-contract due work with only currency or tax-source differences splits under financial_constraint without needing export-shape drift
    • this keeps the grouping contract readable at the domain seam before any broader runtime or DB-backed grouping work lands: PO scope and financial scope remain independent split axes layered on top of the same invoice-window identity
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
  • (2026-03-18) DB-backed invoice API coexistence now closes T260 and fixes a live recurring-detail hydration bug:

    • server/src/test/integration/api/invoiceService.recurringCoexistence.integration.test.ts now seeds one historical flat invoice plus one canonical detail-backed invoice in the live schema, then proves InvoiceService.getById(...) can read both through the same staged-rollout API seam without forcing the historical invoice to synthesize recurring detail metadata
    • while landing that test, packages/billing/src/models/invoice.ts needed a production fix: DB-backed invoice_charge_details.service_period_* values may arrive as Date objects, and the old summary projection only handled strings, which left parent service_period_start|service_period_end null for canonical rows in real integrations even though the detail periods existed
    • the model now normalizes recurring detail dates before building both recurring_detail_periods[] and the parent summary range, so DB-backed readers match the unit-test contract instead of depending on driver-specific value shapes
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/api/invoiceService.recurringDetailProjection.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/api/invoiceService.recurringCoexistence.integration.test.ts --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Hydrated/manual invoice coexistence coverage now closes T200 and T212:

    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now seeds a recurring parent charge with intentionally stale header-like service_period_* / billing_timing values plus two canonical detail rows, then proves Invoice.getById(...) replaces the parent values with the detail-backed summary range and mixed-timing projection
    • the assertion that matters for downstream consumers is now explicit: recurring_projection.source = canonical_detail_rows and recurring_detail_periods[] remain authoritative, while parent service_period_start|service_period_end|billing_timing are only the compatibility summary surface after hydration
    • server/src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts now proves manual adjustments and targeted manual discounts can be added alongside an existing recurring charge without mutating that recurring charges canonical period fields or stamping period fields onto the manual rows
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts --coverage.enabled false
  • (2026-03-18) DB-backed credit reconciliation coverage now closes T177 without relying on the older negative-invoice infrastructure harness:

    • server/src/test/integration/billing/creditReconciliation.integration.test.ts now seeds a negative-invoice credit issuance plus a later credit application against a positive invoice in the live DB schema, then runs validateCreditTrackingRemainingAmounts(...) against those persisted rows
    • the test deliberately uses canonical recurring timing at the reader seam instead of the old header-only assumption: it seeds invoice_charge_details.service_period_start|service_period_end|billing_timing, and its mocked Invoice.getById(...) projects those detail periods back onto parent charge rows before reconciliation metadata is summarized
    • this replaced an abandoned attempt to reuse server/src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts; that infrastructure path kept dragging in unrelated invoice-finalization, UI, and module-resolution dependencies before the credit-reconciliation assertion became trustworthy
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billing/creditReconciliation.integration.test.ts --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Additional materialized-ledger and export-reader coverage now closes T178, T273, T275, T281, T329, and T330:

    • server/src/test/integration/accounting/exportDashboard.integration.test.ts now explicitly claims the DB-backed stored-reader seam for export lines: mixed historical fallback rows and canonical recurring-detail-backed rows reread through getBatchWithDetails(...) without collapsing their persisted service-period semantics
    • server/src/test/integration/accounting/invoiceSelection.integration.test.ts now explicitly claims the export-preview persistence seam: createBatchFromFilters(...) preserves canonical recurring detail periods, summary bounds, and source metadata when preview lines become stored export lines for downstream inspection
    • server/src/test/unit/billing/invoiceGeneration.zeroDollarFinalization.test.ts now tags the zero-dollar recurring finalization seam directly as T210/T273, making the previously documented zero-dollar checkpoint executable against the live persistence/finalization branch rather than leaving it only in scratchpad prose
    • packages/types/src/recurringServicePeriodRecord.typecheck.test.ts now explicitly claims T281 alongside T341, which keeps the shared persisted-record vocabulary anchored to one executable contract for obligation linkage, cadence owner, boundaries, provenance, and lifecycle state
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks that materialized service periods are in-scope v1 behavior rather than a deferred follow-on, and that the expanded plan still preserves implementation-grade breadth after adding editable future billing objects
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts src/test/unit/billing/invoiceGeneration.zeroDollarFinalization.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/accounting/exportDashboard.integration.test.ts src/test/integration/accounting/invoiceSelection.integration.test.ts src/test/integration/accounting/servicePeriodProjectionValidation.integration.test.ts -t "T178|T275|T270" --hookTimeout 600000 --coverage.enabled false
        • first pass proved T178 and T275 while exposing a real schema-harness issue in the new T270 seed data: invoice_charge_details.config_id is non-null in the live schema, so the validation fixture had to stop inserting nulls there
  • (2026-03-18) Plan-integrity coverage now closes T179, T180, T279, and T280:

    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now reads SCRATCHPAD.md, features.json, and tests.json directly so the docs contract suite can assert both the qualitative replan posture and the quantitative checklist breadth instead of relying on memory or manual review
    • the new assertions lock the system-wide PRD blast radius, the scratchpads explicit recursive decomposition and second-pass critique, and the current implementation-grade checklist size/tail contract (270 features through F270, 349 tests through T332)
    • this keeps the plan itself executable as a rollout artifact: if a later edit accidentally collapses the scope back toward billing-engine-only thinking or silently truncates the checklist, the docs suite now fails immediately
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-18) DB-backed contract-cadence and mixed-cadence billing sanity now closes T140, T175, and T176:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now lets the existing selector-input contract-cadence integration explicitly claim the contract-owned detail-persistence and mid-month monthly execution checks under one DB-backed seam: T140/T175/T252
    • that same file now adds a dedicated mixed-cadence grouping integration which inserts one client-cadence persisted row plus one contract-cadence persisted row on the same [2025-01-01, 2025-02-01) window, runs generateInvoice(cycleId), and proves both detail rows land on a single invoice candidate when the documented grouping rules allow it
    • the mixed-cadence fixture needed an explicit horizon pairing of targetHorizonDays: 32 with replenishmentThresholdDays: 15; the materializers reject a low-water threshold equal to or above the horizon target
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T140/T175/T252" --hookTimeout 600000 --coverage.enabled false
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T176" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) The final open plan-surface follow-ons are now explicit, which closes F268, F269, F270, T317, T318, and T319:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_TROUBLESHOOTING.md now gives operators one troubleshooting seam for generation failures, override conflicts, regeneration triage, and invoice-linkage repair without pretending those problems should be solved by ad hoc row mutation
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RICHER_SERVICE_PERIOD_EDITING_FOLLOW_ON.md now fences richer editing out of recurring v1 explicitly: split/merge, bulk schedule transforms, and mass editing remain deferred until the narrow boundary_adjustment|skip|defer surface proves insufficient in production
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/CROSS_DOMAIN_SERVICE_PERIOD_LEDGER_FOLLOW_ON.md now makes the cross-domain boundary explicit for the materialized ledger itself: time, usage, materials, manual invoices, credits, and prepayment artifacts do not quietly join the recurring ledger just because recurring v1 now has one
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks those three artifacts directly so the remaining out-of-scope boundaries stay deliberate instead of drifting back into implicit future work
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-18) The first DB-backed persisted-ledger lifecycle validation now exists, which closes F267 and T316:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now carries T316, which persists real recurring_service_periods rows under the live schema, supersedes a generated row through an explicit boundary edit, regenerates a later future slot while preserving the edited row, and then links the billed edited revision to a real invoice_charge_details row
    • the integration checkpoint intentionally validates the staged-rollout ledger seam instead of claiming full edited-period invoice-generation cutover: the test proves database selection eligibility, supersession state, regenerated future continuity, and billed linkage persistence without depending on later runtime/UI passes that own edited-period invoice explanations
    • the DB-backed test also exposed one concrete implementation gotcha that the earlier domain-only helpers could not: persisted-record helpers need UUID recordIdFactory inputs when they are used against the physical recurring_service_periods table because the live schema stores record_id as uuid
    • focused validation for this checkpoint used:
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T316" --hookTimeout 600000 --coverage.enabled false
      • npx tsc --pretty false --noEmit -p server/tsconfig.json
        • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not by the new persisted-ledger integration test
  • (2026-03-18) Downstream persisted-period contracts are now explicit across charge-family behavior, bucket semantics, post-materialization lifecycle, authoring predictability, grouping after edits, and client/support explanations, which closes F261, F262, F263, F264, F265, F266, T311, T312, T313, T314, and T315:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_CHARGE_FAMILIES.md now documents that fixed, product, and license recurring lines share one persisted service-period lifecycle contract even if rating logic still differs by family, keeping F261 aligned with the later runtime cutover rather than inviting family-specific ledger semantics
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_BUCKET_SEMANTICS.md now classifies bucket/allowance behavior explicitly under edited, skipped, deferred, and regenerated periods so v1 does not silently imply unsupported automatic rollover repair when operators change future schedules
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_POST_MATERIALIZATION_LIFECYCLE.md now defines how billed-through enforcement, renewal/replacement, and mutation guards consume persisted rows after materialization, keeping the post-persist lifecycle understandable before the broader DB validation pass
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_AUTHORING_PREDICTABILITY.md now explains the cut line between authored recurrence rules and future editable schedules across templates, presets, and new-line creation so downstream materialization work remains predictable after creation
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_GROUPING.md now documents how due selection and invoice grouping react when future edits move work across invoice windows, including when regrouping is allowed versus when separate invoice candidates must remain split
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EXPLANATIONS.md now defines the support/client explanation policy for generated versus edited periods, skipped/deferred rows, and invoice detail provenance so later UI work can reuse one narrative contract instead of inventing screen-local copy
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-18) Administrative repair and coexistence posture are now explicit, which closes F259, F260, T309, and T310:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_ADMIN_REPAIR.md now defines the first narrow operator repair modes for missing future rows, drifted untouched generated rows, and incorrect billed-history linkage, while preserving the earlier immutability and conflict rules
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_COEXISTENCE.md now makes the historical-versus-future boundary explicit: future schedules can materialize persisted rows while historical invoices still rely on the dual-shape invoice reader contract, and no synthetic historical backfill is implied
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks both artifacts directly so later repair or migration work cannot silently reopen historical rewrite assumptions or blur schedule drift with invoice-linkage correction
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-18) Pre-save authoring previews now show illustrative future materialized periods, which closes F258 and T308:

    • packages/billing/src/components/billing-dashboard/contracts/recurringAuthoringPreview.ts now generates materializedPeriods[] alongside the existing cadence-owner, first-invoice, and partial-period explainer copy; the preview rows are illustrative, but they come from the same client-cadence and contract-cadence materialization helpers used elsewhere instead of ad hoc sample text
    • the fixed-fee contract wizard step, contract review step, template fixed-fee step, and template review step now all thread billing frequency into that helper and render the illustrative future service periods plus invoice windows, so staff can see the service-period-first model before the recurring line is saved
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_AUTHORING_PREVIEW.md now documents the explicit rule: unsaved work gets explanatory future-period examples generated from materialization logic, not fake persisted rows
    • focused validation for this checkpoint used:
      • cd server && npx vitest run ../packages/billing/tests/recurringAuthoringPreview.test.ts ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts ../packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Billing-staff operational inspection now has an explicit shared view contract, which closes F257 and T300:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodOperationalView, IRecurringServicePeriodOperationalViewSummary, and IRecurringServicePeriodOperationalViewRow, so upcoming-period inspection has one typed read model instead of every later dashboard surface inventing its own row contract
    • shared/billingClients/recurringServicePeriodOperationalView.ts now projects the existing future-listing query and display-state contract into one operational view: deterministic upcoming rows, lifecycle-aware display badges, and summary counts for generated versus exception rows (edited|skipped|locked) before invoice generation
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_OPERATIONAL_VIEWS.md now documents the intended upcoming-period inspection seam and RECURRING_SERVICE_PERIOD_LISTING.md was tightened so the earlier listing pass no longer falsely implies this operational-view layer is still deferred
    • server/src/test/unit/billing/recurringServicePeriodOperationalView.domain.test.ts now proves the shared view excludes billed history by default while still surfacing generated and skipped future rows with summary counts, and server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks the new artifact directly
    • pass-0-source-inventory.json was refreshed in the same checkpoint because the runtime-persisted selection test added another persisted-period reader reference and the docs contract suite intentionally keeps that inventory source-backed
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodOperationalView.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Live recurring runtime now consumes persisted service-period selections before falling back to legacy derivation, which closes F256, T292, and T307:

    • packages/billing/src/actions/invoiceGeneration.ts now marks due recurring selections as recurringTimingSelectionSource: 'persisted' for both billing-cycle-backed invoice windows and selector-input execution windows, so invoice generation stops treating due-period lookup as advisory metadata and instead passes it as the authoritative runtime schedule input
    • packages/billing/src/lib/billing/billingEngine.ts now attempts to load due rows directly from recurring_service_periods when selecting recurring timing for a billing window, converts those rows into runtime selections for fixed, product, and license charge paths, and only falls back to legacy derived timing when the persisted ledger has not been materialized yet or the relation is absent during staged rollout
    • persisted-mode timing resolution is now intentionally strict at the charge-family seam: once a caller says the selections are persisted, missing per-line selections no longer trigger ad hoc service-period derivation inside resolveRecurringChargeTiming(...), which keeps fixed, product, and license runtime behavior aligned on one authoritative schedule source
    • server/src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts now locks the selection handoff for billing-cycle and selector-input billing, while server/src/test/unit/billing/billingEngine.persistedRecurringSelections.test.ts proves fixed, product, and license selections all hydrate from persisted records with the expected service-period coverage behavior
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts src/test/unit/billing/invoiceGeneration.emptyResult.test.ts src/test/unit/billing/invoiceGeneration.preview.test.ts src/test/unit/billing/billingEngine.persistedRecurringSelections.test.ts src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Regeneration-trigger classification is now explicit, which closes F254, T304, and T305:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodRegenerationTriggerInput, IRecurringServicePeriodRegenerationDecision, explicit trigger sources/kinds, and regeneration scopes so later jobs and repositories do not need to infer whether a source edit should rebuild future persisted rows
    • shared/billingClients/recurringServicePeriodRegenerationTriggers.ts now classifies the first v1 trigger families directly: recurrence-shaping contract-line edits use source_rule_changed, assignment activity-window edits use activity_window_changed, cadence-owner changes use cadence_owner_changed plus replace_schedule_identity, and client billing-schedule edits use billing_schedule_changed scoped to client_cadence_dependents
    • the same helper now makes the non-trigger boundary explicit: pricing-only edits do not regenerate persisted periods because they affect future billing amounts rather than future service-period or invoice-window identity
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_REGENERATION_TRIGGERS.md now documents those trigger families, the preserve-edits/preserve-billed-history safety invariants, and the deliberate boundary with later live repository/job wiring
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodRegenerationTriggers.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The source-rule versus override cut line is now queryable, which closes F255 and T306:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodAuthorityBoundary plus explicit authority layers, change channels, future effects, and authority subjects so later UI/API/runtime work can answer whether a concern belongs to source cadence rules, a materialized override, or corrective ledger state
    • shared/billingClients/recurringServicePeriodAuthorityBoundary.ts now centralizes that answer: cadence owner, billing frequency, due position, and activity windows remain source_rule; service-period/invoice-window edits plus skip/defer remain materialized_override; lifecycle state, invoice linkage, and provenance remain ledger_state
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_SOURCE_OVERRIDE_BOUNDARY.md now documents the practical product rule explicitly: future movement should be explainable as either a source-rule change, an explicit future-row override, or a corrective history flow
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodAuthorityBoundary.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted-period governance now has one shared permission and audit-policy contract, which closes F253 and T303:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines governance actions, permission keys, audit events, and IRecurringServicePeriodGovernanceRequirement, so future controllers and dashboard actions do not need to invent their own authorization/audit vocabulary for viewing, editing, skipping, regenerating, or correcting service periods
    • shared/billingClients/recurringServicePeriodGovernance.ts now combines lifecycle-aware mutation legality with explicit governance metadata: view remains separately permissioned and non-audited by default, while edit/skip/defer/regenerate/correction flows all carry explicit audit-event names even when a lifecycle state still blocks the mutation
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_GOVERNANCE.md now documents that boundary explicitly and keeps real role assignment, audit payload schemas, and controller/database wiring deferred to later passes
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodGovernance.domain.test.ts src/test/unit/billing/recurringServicePeriodDisplayState.domain.test.ts src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted-period lifecycle states now have one shared UI-affordance contract, which closes F252 and T302:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodDisplayState and shared display tones so future dashboard rows and badges can consume one stable presentation contract instead of inventing local label semantics
    • shared/billingClients/recurringServicePeriodDisplayState.ts now maps lifecycle plus provenance into explicit UI state metadata: Generated, Edited, Skipped, Locked, Billed, Superseded, and Archived, with operational detail copy and additive provenance-driven reasonLabel strings such as Deferred to a later invoice window
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_UI_STATES.md now documents the badge/detail contract and its deliberate boundary with later permissions, audit identity, and concrete dashboard-layout work
    • RECURRING_SERVICE_PERIOD_EDIT_SURFACES.md was tightened in the same checkpoint so F251 no longer claims state badges are still deferred behind F252
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodDisplayState.domain.test.ts src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Future persisted-period edits now have an explicit UI/API transport contract, which closes F251 and adds/closes T349 while intentionally leaving T295 and T301 for later persisted-editing screens and broader state affordances:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodEditRequest, IRecurringServicePeriodEditRequestContext, IRecurringServicePeriodEditResponse, and structured validation issue codes/fields so future controllers and dashboard forms have one typed request/response seam for boundary_adjustment, skip, and defer
    • shared/billingClients/recurringServicePeriodEditRequests.ts now dispatches those transport requests onto the existing edit primitives and returns explicit success payloads with supersededRecord, editedRecord, and provenance, while mapping lower-level validation failures into structured issues such as missing_deferred_invoice_window, continuity_gap_before, and record_mismatch
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_SURFACES.md now documents that boundary explicitly: request contract, success response, validation issue surface, and the deliberate deferral of repository/controller wiring, dashboard state badges, and permission/audit policy to F252-F259
    • tests.json now adds T349 because the original T301 also depends on the later billing-staff editing surfaces and state affordances in F252; the new test locks the shared transport contract itself without pretending the dashboard or controllers already exist
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodEditRequests.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Future persisted-period listing now has an explicit query contract, which closes F250 and adds/closes T348 while intentionally leaving T300 for later UI/dashboard surfaces:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodListingQuery plus the default listing lifecycle-state set generated|edited|skipped|locked, making future-ledger inspection a first-class read contract instead of a side effect of due selection
    • shared/billingClients/recurringServicePeriodListing.ts now defines the first future-listing helper: it filters by tenant, asOf, optional schedule/cadence/due-position/charge-family scope, excludes billed/superseded/archived rows by default, and keeps deterministic chronological ordering
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_LISTING.md now documents that listing is intentionally independent from invoice generation and due selection, so later API and dashboard work can build on one shared read seam
    • tests.json now adds T348 because the original T300 also depends on later billing-staff UI surfaces in F257; the new test locks the listing contract itself without pretending the dashboard already exists
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodListing.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) Regeneration conflicts against preserved user edits are now explicit, which closes F249 and T299:

    • shared/billingClients/regenerateRecurringServicePeriods.ts now returns additive conflicts metadata instead of silently discarding disagreements when preserved override rows no longer match regenerated source candidates
    • the first v1 conflict kinds are missing_candidate, service_period_mismatch, invoice_window_mismatch, and activity_window_mismatch, which keeps preserved edits active while making the disagreement queryable for later operator tooling
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_CONFLICTS.md now documents that conservative handling rule explicitly: preserve the edited row, surface the conflict, and leave repair/merge tooling for later passes
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodRegenerationConflict.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) Neighbor-aware edit validation is now explicit, which closes F248 and T298:

    • shared/billingClients/recurringServicePeriodEditValidation.ts now defines the first continuity guard for edited future rows: when sibling rows on the same scheduleKey are supplied, edits are rejected for gap before, overlap before, gap after, and overlap after states instead of silently corrupting the schedule
    • shared/billingClients/editRecurringServicePeriodBoundaries.ts and shared/billingClients/skipOrDeferRecurringServicePeriod.ts now call the same validator when caller-side sibling context is present, which keeps the boundary-adjustment and disposition helpers aligned on one continuity rule instead of each inventing their own local checks
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_VALIDATION.md now documents the same-schedule adjacency rule and its explicit boundary with the later regeneration-conflict and UI transport passes
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodEditValidation.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) Split and merge are now explicitly fenced out of v1, which closes F247 and adds/closes T347 while intentionally leaving T297 for any later advanced-edit pass:

    • shared/billingClients/recurringServicePeriodEditCapabilities.ts now makes the v1 edit surface executable instead of leaving it as docs only: supported operations are boundary_adjustment, skip, and defer, while split and merge fail fast as unsupported v1 operations
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_OPERATIONS.md now carries an explicit Unsupported In V1 section so later UI/API work cannot quietly assume split/merge support before a deliberate follow-on pass exists
    • tests.json now adds T347 because the original T297 also depends on the later continuity semantics in F248; the new test locks the v1 non-support decision directly
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodEditCapabilities.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) Skip and defer are now explicit persisted-period edit operations, which closes F246 and adds/closes T346 while intentionally leaving T296 for the later continuity pass:

    • shared/billingClients/skipOrDeferRecurringServicePeriod.ts now defines the first explicit disposition edits for future persisted rows: skip creates a superseding skipped revision with reasonCode = skip, while defer creates a superseding edited revision with reasonCode = defer and an explicitly moved invoice window
    • the helper deliberately stays narrower than the later neighbor-aware validation work: it reuses the existing mutation guard, requires a new deferred invoice window instead of silently reusing the prior one, and keeps service-period continuity / invoice-linkage interplay for F248
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_OPERATIONS.md now documents boundary_adjustment, skip, and defer together as the minimal v1 edit surface, while split/merge and UI/API editing remain sequenced later
    • tests.json now adds T346 because the original T296 also depends on the later overlap/gap validation pass in F248; the new test locks the explicit skip/defer revision semantics without claiming broader continuity validation already exists
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodDisposition.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) The minimal v1 boundary-adjustment edit operation is now explicit, which closes F245 and adds/closes T345 while intentionally leaving T295 for the later continuity + UI passes:

    • shared/billingClients/editRecurringServicePeriodBoundaries.ts now defines the first explicit edit primitive for persisted service periods: mutable future rows can be superseded by a new edited revision, provenance.kind = user_edited, and the helper infers boundary_adjustment, invoice_window_adjustment, or activity_window_adjustment reason codes instead of hiding edits behind source-rule changes
    • the helper stays intentionally narrow at this pass: it reuses the existing immutability guard, validates only local half-open boundary integrity plus in-period activity clipping, rejects no-op edits, and does not yet attempt neighbor continuity or transport-surface validation that belongs to F248 and F251
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_EDIT_OPERATIONS.md now documents that boundary_adjustment is the only supported v1 edit operation at this checkpoint, while skip/defer, split/merge, and UI/API editing remain sequenced later
    • tests.json now adds T345 because the original T295 also depends on the later continuity-validation and editing-surface work in F248 and F251; the new test locks the edit primitive itself without pretending the broader pass already exists
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodBoundaryEdit.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) The first billed-history-safe backfill planner now exists, which closes F244, T286, and T294:

    • shared/billingClients/backfillRecurringServicePeriods.ts now defines the v1 initialization rule for legacy recurring lines before runtime cutover: future candidate rows are normalized onto provenance.reasonCode = backfill_materialization, billed-history boundaries come from legacy billed-through data plus any already-linked persisted rows, and candidates overlapping that boundary are rejected instead of being silently clipped
    • the same helper now makes partial-rollout backfill explicit instead of accidental: already-billed rows are retained unchanged, equivalent future rows are retained unchanged, and untouched generated future rows that drift from the current candidate schedule are regenerated with reasonCode = backfill_realignment while preserving the earlier row as superseded
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_BACKFILL.md now documents the historical-boundary rule, backfill provenance, and the deliberate boundary that historical invoices are not rehydrated into synthetic persisted rows during v1 initialization
    • server/src/test/unit/billing/recurringServicePeriodBackfill.domain.test.ts now locks both halves directly: billed client-cadence history fences off future inserts without mutation, and stale future generated rows realign under explicit backfill provenance while billed history remains unchanged
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodBackfill.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-18) Persisted-schedule parity comparison is now explicit, which closes F243 and T293:

    • shared/billingClients/recurringServicePeriodKeys.ts now centralizes canonical scheduleKey and periodKey generation, and both materializers now use that helper so persisted schedule identity cannot drift from later comparison logic
    • shared/billingClients/recurringServicePeriodParity.ts now defines the staged-cutover parity seam for materialized schedules: legacy derived selections normalize onto scheduleKey + periodKey, active persisted rows normalize onto the same key, and schedule parity reports only the rollout-significant drift kinds missing_persisted_period, unexpected_persisted_period, and invoice_window_mismatch
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_PARITY_COMPARISON.md now documents that this is schedule parity rather than invoice parity, and that F256 still owns the later runtime cutover from derived timing to persisted-row selection
    • pass-0-source-inventory.json was refreshed in the same checkpoint because the new parity helper became a live service-period reader; the docs contract suite now expects that helper in the persisted-reader inventory instead of silently drifting
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodParity.domain.test.ts src/test/unit/billing/materializeClientCadenceServicePeriods.domain.test.ts src/test/unit/billing/materializeContractCadenceServicePeriods.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The persisted due-selection query contract is now explicit, which closes F242 and adds/closes T344 while intentionally leaving T292 for F256 runtime adoption:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringServicePeriodDueSelectionQuery plus the default eligible lifecycle-state set generated|edited|locked, making the persisted-row selector contract explicit instead of leaving it as an implied future repository query
    • shared/billingClients/recurringServicePeriodDueSelection.ts now defines the v1 selector seam: callers must resolve scheduleKeys[] first, exact [windowStart, windowEnd) invoice-window matching is required, billed/linkage-bearing rows are excluded, and deterministic sort order is service-period start/end then obligation id then revision
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_DUE_SELECTION.md now documents the contract boundary clearly: F242 defines the selector inputs and filters, while F256 remains the later pass that switches live invoice generation off the derived billing-engine schedule and onto persisted rows
    • tests.json now adds T344 because the original T292 also covers the later runtime cutover in F256; the new test locks the selector contract itself without pretending invoice generation already consumes persisted rows end to end
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodDueSelection.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted service-period invoice linkage is now explicit, which closes F241 and T291:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now adds optional invoiceLinkage metadata on IRecurringServicePeriodRecord, with one additive linkage object carrying invoiceId, invoiceChargeId, invoiceChargeDetailId, and linkedAt
    • shared/billingClients/recurringServicePeriodInvoiceLinkage.ts now defines the v1 linkage helper boundary: linking a record to an invoice_charge_details row stamps the record as billed, preserves idempotent same-row relinks, and rejects conflicting relinks until a later invoice_linkage_repair flow handles them explicitly
    • server/migrations/20260318143000_add_invoice_linkage_to_recurring_service_periods.cjs now adds additive linkage columns on recurring_service_periods plus the first integrity rules for billed-history traceability: linkage columns are all-null or all-present together, invoice_charge_detail_id is tenant-unique, and linked rows must already be in billed state
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_INVOICE_LINKAGE.md now documents the billed-history linkage contract and its explicit boundary with the later due-selection/runtime adoption work in F242-F244
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodInvoiceLinkage.domain.test.ts src/test/unit/migrations/recurringServicePeriodInvoiceLinkageMigration.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Locked/billed immutability is now explicit, which closes F240 and T290:

    • shared/billingClients/recurringServicePeriodMutations.ts now defines the v1 mutation guard for persisted service-period rows: unlocked future rows may still follow normal edit/regeneration flows, while locked and billed rows reject ordinary edits and allow only the narrow corrective operations invoice_linkage_repair and archive
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_IMMUTABILITY.md now documents that billed coverage remains audit history instead of mutable schedule draft state, while also keeping the invoice-linkage repair boundary explicit for the later linkage pass
    • server/src/test/unit/billing/recurringServicePeriodMutations.domain.test.ts now locks the allowed/disallowed operations directly for generated, locked, and billed rows, and server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks the immutability artifact
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodMutations.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The first regeneration and override-preservation algorithm now exists, which closes F238, F239, T288, and T289:

    • shared/billingClients/regenerateRecurringServicePeriods.ts now defines the v1 regeneration rule for future active rows: untouched generated slots can be refreshed into regenerated revisions, replaced rows become superseded, and leftover candidate slots stay new generated rows
    • the same helper now makes override preservation explicit instead of accidental: user_edited, repair, edited, locked, and billed future rows are preserved as-is, and the candidate slot that would have overwritten them is intentionally discarded until a later explicit conflict-resolution flow exists
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_REGENERATION.md now documents the slot-order regeneration policy, period-key/revision continuity, and the deliberate v1 boundary that preserved overrides are not merged automatically
    • server/src/test/unit/billing/recurringServicePeriodRegeneration.domain.test.ts now locks both halves directly: changed untouched rows regenerate into a new revision while user-edited rows remain preserved and unmodified
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodRegeneration.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The first contract-cadence materialization helper now exists, which closes F237 and T287:

    • shared/billingClients/materializeContractCadenceServicePeriods.ts now wraps the existing contract-anniversary generators and resolveContractCadenceInvoiceWindowForServicePeriod(...), producing persisted future record candidates for contract-owned recurring obligations without waiting for later invoice-selection cutover work
    • the helper keeps contract-owned timing explicit at the persisted-record seam: schedule keys are contract owned, timing metadata preserves the anniversary anchor, advance invoice windows match the contract-owned service period, and arrears invoice windows land on the next contract-owned window after the covered period ends
    • server/src/test/unit/billing/materializeContractCadenceServicePeriods.domain.test.ts now locks the monthly contract-owned materialization path directly for both advance and arrears, proving the additive record shape instead of only re-testing raw boundary generation
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/materializeContractCadenceServicePeriods.domain.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The first client-cadence materialization helper now exists, which closes F236 and adds/closes T343 while intentionally leaving T286 for F244 backfill/no-billed-history rules:

    • shared/billingClients/materializeClientCadenceServicePeriods.ts now composes the existing client-cadence boundary generator with the new horizon policy and the persisted-record contract, producing additive future record candidates for client-owned recurring obligations instead of only ephemeral runtime periods
    • the helper preserves parity semantics instead of inventing new date math: service-period boundaries still come from generateClientCadenceServicePeriods(...), advance invoice windows stay aligned to the same period, arrears invoice windows resolve to the next client-cadence window, and generated rows use the explicit generated provenance plus the shared persisted-record identity vocabulary
    • server/src/test/unit/billing/materializeClientCadenceServicePeriods.domain.test.ts now locks both advance and arrears mapping behavior directly, including schedule keys, canonical invoice-window mapping, generated provenance, and horizon coverage
    • tests.json now adds T343 because the original T286 also covers the later F244 backfill rule about not mutating billed history; that acceptance surface is still intentionally open until the dedicated backfill semantics land
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/materializeClientCadenceServicePeriods.domain.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The v1 generation-horizon policy is now explicit, which closes F235 and T285:

    • shared/billingClients/recurringServicePeriodGenerationHorizon.ts now defines the first operational horizon contract for persisted recurring periods: initial materialization/backfill targets 180 days of future coverage, steady-state replenishment triggers at the 45-day low-water mark, and continuity issues are surfaced explicitly instead of being silently treated as ordinary replenishment
    • the same helper now makes the continuity invariant executable before ledger-writing code lands: findRecurringServicePeriodContinuityIssues(...) detects gap and overlap states under the canonical half-open model, while assessRecurringServicePeriodGenerationCoverage(...) reports whether the current future ledger both meets the target horizon and has fallen low enough to require replenishment
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_GENERATION_HORIZON.md now documents the target horizon, low-water threshold, whole-period overshoot rule, and the explicit boundary that continuity repair is separate from ordinary replenishment
    • server/src/test/unit/billing/recurringServicePeriodGenerationHorizon.domain.test.ts now locks the helper behavior directly, while server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks the new horizon artifact so the executable helper and the plan policy stay aligned
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodGenerationHorizon.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The persisted-service-period provenance model is now explicit, which closes F234 and T284:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now turns IRecurringServicePeriodRecordProvenance into a discriminated union with explicit reason-code catalogs for generated, user_edited, regenerated, and repair, so later materialization/edit/regeneration work no longer depends on a loose optional-string bag for provenance semantics
    • shared/billingClients/recurringServicePeriodProvenance.ts now provides the reusable v1 helper layer: isRecurringServicePeriodProvenanceReasonCode(...), isRecurringServicePeriodProvenanceDivergent(...), and validateRecurringServicePeriodProvenance(...) make the required-versus-optional field rules executable before repositories and UI flows start writing these records for real
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_PROVENANCE.md now documents the provenance kinds, required-field matrix, reason-code catalog, and the operational meaning of divergence from untouched cadence rules; PERSISTED_SERVICE_PERIOD_RECORD.md now points at that artifact instead of leaving provenance requirements as future tense
    • server/src/test/unit/billing/recurringServicePeriodProvenance.domain.test.ts now locks the shared helper/domain behavior directly, while server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts locks the new provenance artifact so the typed helper and the plan language cannot drift apart quietly
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodProvenance.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The persisted-period lifecycle model is now explicit, which closes F233 and T283:

    • shared/billingClients/recurringServicePeriodLifecycle.ts now defines the authoritative v1 state machine for persisted service-period records, including the allowed transition map, terminal-state set, and reusable canTransitionRecurringServicePeriodState(...) / isRecurringServicePeriodStateTerminal(...) helpers
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_LIFECYCLE.md now documents the state meanings, conservative allowed transitions, terminal-state policy, and v1 invariants so later edit/regeneration/billing-linkage work has one lifecycle baseline instead of ad hoc state semantics
    • packages/billing/src/index.ts now re-exports the lifecycle helpers/constants from the billing package surface so downstream consumers can adopt the same transition contract without reaching into shared internals directly
    • server/src/test/unit/billing/recurringServicePeriodLifecycle.domain.test.ts now locks the transition map directly, while server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks the lifecycle artifact so the documented state model and the executable helper cannot drift apart quietly
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodLifecycle.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) The physical persisted-ledger table now exists, which closes F232, closes T282, and adds/closes T342:

    • server/migrations/20260318120000_create_recurring_service_periods.cjs now creates recurring_service_periods as the first physical ledger table for materialized recurring periods, flattening the F231 record contract into row columns for identity, obligation linkage, cadence ownership, service/invoice boundaries, activity clipping, provenance, and audit timestamps
    • the migration now enforces the first integrity boundary directly in the database: check constraints cover cadence owner, due position, lifecycle state, obligation type, charge family, provenance kind, positive revision numbers, valid [start, end) service/invoice windows, valid optional activity-window clipping, and supersedes_record_id <> record_id
    • the migration also adds the first lookup posture for later materialization/runtime work: a unique revision key on (tenant, schedule_key, period_key, revision) plus tenant-scoped indexes for schedule lookup, obligation-state scans, and due selection by invoice window
    • server/src/test/unit/migrations/recurringServicePeriodsMigration.test.ts now executes the migration against a fake schema contract so the table shape, constraints, and index names are locked directly instead of being left as migration prose
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PERSISTED_SERVICE_PERIOD_RECORD.md now documents the physical recurring_service_periods landing and pass-0-source-inventory.json was refreshed because the docs contract inventory intentionally tracks all live service_period_* references, including the new migration and migration test
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/migrations/recurringServicePeriodsMigration.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p server/tsconfig.json
        • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not by the recurring-service-period migration
  • (2026-03-18) The first materialized-ledger checkpoint now has one authoritative logical record contract, which closes F231 and adds/closes T341:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IPersistedRecurringObligationRef, IRecurringServicePeriodRecordProvenance, IRecurringServicePeriodRecord, RecurringServicePeriodLifecycleState, and RecurringServicePeriodProvenanceKind, so future schema/runtime work can target one shared persisted-record vocabulary instead of inventing ad hoc shapes
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PERSISTED_SERVICE_PERIOD_RECORD.md now defines the logical record shape explicitly: recordId, scheduleKey, periodKey, revision, obligation linkage, cadence ownership, [start, end) service/invoice boundaries, provenance, and lifecycle state, while also naming the follow-on boundaries for F232, F233, and later invoice-linkage/runtime adoption work
    • server/src/test/test-utils/recurringTimingFixtures.ts now includes persisted-record fixture builders so the later materialized-ledger passes can reuse one contract-aware test seam instead of recreating record shapes inline
    • executable coverage for this checkpoint now lives in packages/types/src/recurringServicePeriodRecord.typecheck.test.ts, while server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now also locks the presence and core sections of the new persisted-record artifact
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
      • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-17) The staged cutover, rollback, and follow-on boundary posture is now explicit in the plan artifacts, which closes F224, F225, F226, F227, F228, F229, F230, T253, T254, T255, T256, T257, T258, and T259:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/CUTOVER_SEQUENCE.md now defines the ordered cutover bands for reader-first invoice hydration, writer propagation, scheduler identity, grouping, downstream reporting/portal/export adoption, and rollback/coexistence expectations while historical flat invoices and canonical detail-backed invoices both remain queryable
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RUNBOOK.md now includes dedicated Projection Mismatch Investigation and Authoring-Default Drift Investigation sections, with explicit SQL/source-check guidance for header-versus-detail disagreements and for recurring-default drift across templates, presets, wizard flows, and custom recurring lines
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PRD.md and ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PASS0_RECURRING_TIMING_APPENDIX.md now both carry explicit follow-on boundaries for persisted recurring execution records and invoice-schema versioning, keeping those concerns out of recurring v1 unless rollout pressure or long-lived coexistence justifies reopening them deliberately
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks all seven documentation contracts directly, and pass-0-source-inventory.json was refreshed in the same checkpoint so the docs suite stays aligned with current grep-backed source references instead of stale inventory entries
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Cleanup-proof source/DB validation now covers the remaining dropped-table, authoritative-reader, and selector-input scheduler seams, which closes F221, F222, F223, T251, T252, T271, T272, and adds T340 for the missing DB half of F221:

    • shared/billingClients/templateClone.ts no longer reads contract_template_line_mappings.custom_rate; template-clone custom-rate lookup now reads the authoritative contract_template_lines.custom_rate, which removes the last live shared-package dependency on a dropped recurrence mapping table
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now scans shared/billingClients alongside packages/billing/src and server/src/lib, so the dropped-table source contract covers shared runtime helpers instead of only package/server code
    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now carries F221 DB validation proving recurring invoice generation still succeeds while contract_line_terms, contract_line_mappings, and contract_template_line_mappings are absent from the live schema; tests.json gained T340 because the existing T247 checklist item only covered source absence
    • packages/billing/src/actions/invoiceGeneration.ts and packages/billing/src/lib/billing/billingEngine.ts now distinguish real billing-cycle UUID bridges from selector-input-only execution windows: selector windows no longer get forced into invoices.billing_cycle_id or reloaded through client_billing_cycles
    • packages/billing/src/lib/billing/billingEngine.ts now exposes calculateBillingForExecutionWindow(...), which reuses the canonical prepared-period path without assuming the execution window can be looked up by billing-cycle UUID
    • comparison-mode drift logging now skips execution windows that have no real billing-cycle bridge instead of trying to replay legacy billing against a typed contract-cadence identity
    • server/src/test/integration/accounting/invoiceSelection.integration.test.ts passes T251, proving preview selection prefers canonical detail periods over invoice header periods when both exist
    • packages/billing/tests/authoritativeRecurringReaders.servicePeriods.wiring.test.ts passes T272, locking the source-side preference for canonical detail periods over header fallbacks
    • server/src/test/unit/jobs/generateInvoiceHandler.recurringExecutionIdentity.test.ts passes T271, and server/src/test/integration/billingInvoiceTiming.integration.test.ts passes T252, proving selector-input contract-cadence execution can run and persist invoices with billing_cycle_id = null instead of being blocked on a billing-cycle-only scheduler assumption
    • focused validation for this checkpoint used:
      • cd server && npx tsc --pretty false --noEmit -p ../packages/billing/tsconfig.json
      • cd server && npx vitest run ../packages/billing/tests/authoritativeRecurringReaders.servicePeriods.wiring.test.ts src/test/unit/jobs/generateInvoiceHandler.recurringExecutionIdentity.test.ts src/test/unit/billing/recurrenceStorageModel.contract.test.ts --coverage.enabled false
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T252" --hookTimeout 600000 --coverage.enabled false
      • PGPASSWORD=postpass123 psql -h 127.0.0.1 -p 57433 -U postgres -d postgres -c 'DROP DATABASE IF EXISTS test_database;'
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/accounting/invoiceSelection.integration.test.ts -t "T251" --hookTimeout 600000 --coverage.enabled false
    • notable harness gotchas:
      • DB-backed suites still require overriding the stale .env.localtest database port from 5438 to 57433
      • re-running invoiceSelection.integration.test.ts back-to-back can leave a stale test_database; dropping it before the isolated rerun avoids the duplicate-DB bootstrap failure
  • (2026-03-17) Partially migrated live recurrence rows now normalize at the remaining client-facing read/write edges, which closes F219 and T249:

    • packages/clients/src/models/clientContract.ts now normalizes contract_lines rereads with normalizeLiveRecurringStorage(...), so client-contract authoring flows stop returning raw legacy-null billing_timing / cadence_owner values
    • packages/clients/src/actions/clientContractLineActions.ts now normalizes partially migrated recurring fields on both sides of the client-line seam: getClientContractLine(...) returns normalized defaults before date serialization, and addClientContractLine(...) now clones from templateRecurringStorage instead of hand-written ?? 'arrears' / 'client' fallbacks
    • packages/client-portal/src/actions/client-portal-actions/client-billing.ts now normalizes the client-portal contract-line read-model before returning it, which keeps portal contract-line details on the same shared compatibility contract as the billing and clients packages
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now claims T249 for this plan and locks those source seams explicitly; packages/clients/src/actions/clientContractLineActions.recurringCompatibility.test.ts adds behavior coverage for normalized reads plus clone writes; packages/client-portal/src/actions/client-portal-actions/client-billing.recurringCompatibility.test.ts adds a focused source wiring guard for the portal seam because importing that action directly still trips the unrelated local @alga-psa/jobs package-resolution issue
    • packages/billing/tests/renewalsQueueActions.schemaReadiness.integration.test.ts had unrelated T249/T250/T251/T254/T255 labels from another effort; those were renamed to RQ249-style labels so this plans checklist IDs stay one-to-one with the billing work
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts ../packages/clients/src/actions/clientContractLineActions.recurringCompatibility.test.ts ../packages/client-portal/src/actions/client-portal-actions/client-billing.recurringCompatibility.test.ts ../packages/billing/tests/renewalsQueueActions.schemaReadiness.integration.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json, and npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
  • (2026-03-17) Stale term-storage references are now described as compatibility only instead of authoritative recurrence storage, which closes F218 and T248:

    • packages/billing/src/actions/contractLineAction.ts now says directly that contract_template_lines.billing_timing is authoritative and contract_template_line_terms survives only as a legacy shadow read/write during staged rollout
    • packages/billing/src/models/contractTemplate.ts, packages/billing/src/repositories/contractLineRepository.ts, and server/src/lib/repositories/contractLineRepository.ts now describe contract_template_line_terms joins as compatibility fallbacks, which removes the remaining model/repository comments that could be read as treating the terms table as the live source of truth
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now includes T248, locking the authoritative-versus-fallback wording against RECURRENCE_STORAGE_MATRIX.md and those source comments
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) Stale dropped recurrence-table reads are now explicitly fenced off instead of being left implicit, which closes F217 and T247:

    • a fresh source inventory across packages/billing/src and server/src/lib shows no live reads or joins against the removed recurrence tables contract_line_terms, contract_line_mappings, or contract_template_line_mappings
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now includes T247, which locks that absence directly while also documenting that contract_template_line_terms.billing_timing remains the one intentional template compatibility fallback listed in AUTHORITATIVE_RECURRENCE_STORAGE_MODEL
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) Mapping and repository write paths no longer silently drop recurring cadence fields, which closes F216 and T246:

    • packages/billing/src/actions/contractLineMappingActions.ts now normalizes template snapshots with normalizeLiveRecurringStorage(...) and carries both billing_timing and cadence_owner through template-line updates, so mapping writes stop preserving only cadence owner while dropping timing
    • packages/billing/src/models/contractLineMapping.ts now treats billing timing as part of the mapping contract on both live and template rows: reads select billing_timing, live updates resolve cadence owner plus timing through resolveRecurringAuthoringPolicy(...), and template fallbacks return the updated timing field instead of silently omitting it
    • packages/billing/src/repositories/contractLineRepository.ts and server/src/lib/repositories/contractLineRepository.ts now normalize single-line rereads with normalizeLiveRecurringStorage(...) and preserve existing billing_timing when updateContractLineRate(...) changes only rate data, removing another pair of repository write seams that previously depended on raw undefined handling
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now includes T246, while packages/billing/tests/contractLineCadenceOwnerCompatibility.wiring.test.ts and packages/billing/tests/contractLineMappingRecurringTiming.wiring.test.ts were refreshed to lock the new helper-based cadence/timing propagation contract directly at the mapping seams
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts ../packages/billing/tests/contractLineCadenceOwnerCompatibility.wiring.test.ts ../packages/billing/tests/contractLineMappingRecurringTiming.wiring.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) Legacy billing_cycle_alignment compatibility defaults now run through one shared resolver across the remaining fixed recurring write and reread seams, which closes F215 and T245:

    • packages/billing/src/components/billing-dashboard/ContractLineDialog.tsx, packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLinePresetConfiguration.tsx, and packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLineConfiguration.tsx now use resolveBillingCycleAlignmentForCompatibility(...) when rehydrating fixed config state and when emitting fixed-config writes, so legacy rows with enable_proration = true but no stored alignment no longer snap back to 'start' in the UI
    • packages/billing/src/actions/contractLineAction.ts, packages/billing/src/actions/contractWizardActions.ts, and packages/billing/src/actions/contractLineMappingActions.ts now use the same helper before writing plan fixed config, client-contract fixed config copies, and template-line fixed snapshots, which removes the last hard-coded 'start' fallbacks from live fixed recurring propagation seams
    • packages/billing/src/repositories/contractLineRepository.ts and server/src/lib/repositories/contractLineRepository.ts now normalize alignment on contract-line and template-line rereads using both stored alignment and enable_proration, keeping readback compatibility consistent with the shared write contract
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now includes T245, locking the helper-based normalization across action, wizard, mapping, UI, and repository seams, and packages/billing/tests/contractLinePresetRecurringAuthoring.wiring.test.ts now expects the resolver instead of the older inline 'start' fallback
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts src/test/unit/api/contractLineService.billingCycleAlignmentCompatibility.wiring.test.ts ../packages/billing/tests/billingCycleAlignmentCompatibility.model.test.ts ../packages/billing/tests/contractLinePresetRecurringAuthoring.wiring.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) Billing-timing default normalization now runs through one shared policy across the remaining recurring write seams, which closes F214 and T244:

    • packages/billing/src/actions/contractLineAction.ts now uses normalizeLiveRecurringStorage(...) and normalizeTemplateRecurringStorage(...) for contract-line readback and template-line lookups, eliminating another pocket of hand-written billing_timing ?? 'arrears' logic at the action seam
    • packages/billing/src/repositories/contractLineRepository.ts and server/src/lib/repositories/contractLineRepository.ts now normalize billing timing before template snapshot writes and before template-to-contract clone writes, so those repository-level propagation seams no longer bypass the shared recurring timing default
    • server/src/lib/api/services/ContractLineService.ts now normalizes template-derived recurring cadence fields before writing a fresh live contract line during replacement/renewal assignment flows, which removes the last live service-layer template clone fallback that still hard-coded arrears
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now includes T244, locking the shared billing-timing default constant plus the fact that wizard, preset, contract-line action, repository, and server assignment write paths all reference the shared normalization helpers instead of inventing local defaults
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.model.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) The recurrence storage contract is now explicit across live lines, template lines, presets, and shared readers, which closes F211, F212, F213, F220, T241, T242, T243, and T250:

    • shared/billingClients/recurrenceStorageModel.ts now defines one authoritative storage model for recurring cadence fields and one set of normalization helpers for live lines, template lines, and presets
    • packages/billing/src/models/contractTemplate.ts, packages/billing/src/models/contractLinePreset.ts, packages/billing/src/repositories/contractLineRepository.ts, server/src/lib/repositories/contractLineRepository.ts, and shared/billingClients/contractLines.ts now reuse that shared storage model instead of open-coding separate recurring defaults at each read seam
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRENCE_STORAGE_MATRIX.md now names the authoritative table for each recurrence field, the fixed-config compatibility split for partial-period fields, and the shared interface shapes that project those fields
    • server/src/test/unit/billing/recurrenceStorageModel.contract.test.ts now locks the new shared model plus template/preset cadence-owner migration backfills, while server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now enforces the new storage matrix artifact and pass-0-source-inventory.json was refreshed so the docs contract stays aligned with live source references
    • focused validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.model.test.ts --coverage.enabled false, npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json, and npx tsc --pretty false --noEmit -p server/tsconfig.json; the server compile remains blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error
  • (2026-03-17) Template fixed-line authoring now captures recurring timing and first-invoice semantics explicitly instead of silently defaulting everything to arrears/no-adjustment, which closes F206 and T235:

    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/TemplateWizard.tsx now carries billing_timing and enable_proration in template wizard state, defaults them to arrears / false, and includes both fields in ContractTemplateWizardSubmission
    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/steps/TemplateFixedFeeServicesStep.tsx now lets authors choose fixed-line Billing Timing, toggle Adjust for Partial Periods, and see first-invoice explainer copy directly in the template authoring step alongside the existing cadence-owner chooser
    • packages/billing/src/actions/contractWizardActions.ts now passes submission.billing_timing into resolveRecurringAuthoringPolicy(...) when creating template-backed fixed, product, hourly, and usage lines, so template-created recurring defaults stop relying on the action-layer arrears fallback when the wizard meant advance
    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/steps/TemplateReviewContractStep.tsx now echoes cadence owner, billing timing, and partial-period adjustment in the review summary so template authors can verify first-invoice semantics before publishing
    • focused coverage now lives in packages/billing/tests/templateWizardBucketOverlay.test.ts and packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts; the runtime wizard test now proves submission carries billing_timing: 'advance' plus enable_proration: true, while the wiring test locks the new template fixed-step and action-layer seams
  • (2026-03-17) Preset authoring/editing now persists the same recurring timing defaults that preset reuse was already replaying, which closes F205 and T234:

    • packages/billing/src/components/billing-dashboard/ContractLineDialog.tsx now persists explicit preset recurrence defaults instead of dropping them at the modal save seam: fixed presets write billing_timing, preset creates/updates write cadence_owner: 'client' during the current rollout, and fixed preset config writes now preserve the active partial-period alignment instead of hard-coding start
    • the preset create/edit modal now promotes the hidden fixed-config compatibility alignment from start to prorated when Adjust for Partial Periods is enabled on a preset that did not already carry a more specific stored alignment, matching the partial-period policy used elsewhere
    • packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLinePresetConfiguration.tsx now exposes Billing Timing on the main fixed preset edit surface, rehydrates stored billing_timing, and persists that value plus explicit client cadence metadata when preset basics are saved
    • focused coverage now lives in packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts and packages/billing/tests/contractLinePresetRecurringAuthoring.wiring.test.ts; the action test now proves preset reuse replays stored advance timing plus partial-period defaults onto the created contract line, while the wiring test locks the create/edit UI payload seams where timing fields had previously been omitted
  • (2026-03-17) Recurring authoring defaults now have one shared policy source instead of per-path fallbacks, which closes F201, F202, F203, F204, T231, T232, and T233:

    • shared/billingClients/recurringAuthoringPolicy.ts now defines the authoritative v1 authoring defaults: cadence owner defaults to client, recurring billing timing defaults to arrears, touched writes preserve stored cadence/timing when omitted, and fixed-line legacy alignment derives through the existing compatibility helper instead of ad hoc inline branches
    • packages/billing/src/models/contractLine.ts no longer silently drops billing_timing on create/update, so contract-wizard-created and custom recurring lines now persist the same timing semantics they author instead of relying on reader-side arrears fallbacks
    • packages/billing/src/actions/contractWizardActions.ts now resolves one shared recurring authoring policy per submission and uses it across fixed, product, hourly, and usage line creation plus fixed-config alignment writes
    • packages/billing/src/actions/contractLinePresetActions.ts now uses the same shared policy for preset-to-contract copies and custom recurring-line creation, which fixes the old custom-line mismatch where missing billing_timing defaulted to advance and fixed enable_proration still wrote billing_cycle_alignment: start
    • packages/billing/src/repositories/contractLineRepository.ts, server/src/lib/repositories/contractLineRepository.ts, packages/billing/src/actions/contractLineAction.ts, and server/src/lib/api/services/ContractLineService.ts now preserve or normalize cadence/timing fields through the same helper on touched writes, reducing live authoring-path drift before the later template/preset cleanup features land
    • packages/billing/tests/fixedContractLineConfiguration.recurringAuthoring.wiring.test.ts now locks the live fixed-line edit seam directly: the inline editor must keep cadence owner on updateContractLine(...), keep billing timing on upsertContractLineTerms(...), and depend on the updated action-layer normalization instead of silently dropping touched timing fields
    • packages/billing/src/components/billing-dashboard/contracts/CreateCustomContractLineDialog.tsx now defaults new custom recurring lines to arrears, matching the rest of the recurring authoring surfaces instead of preselecting a divergent timing mode in the UI
  • (2026-03-17) External tax import and reconciliation consumers now have explicit service-period-first guardrails, which closes F200 and T229:

    • packages/billing/src/services/externalTaxImportService.ts now states the intended policy inline at both import and reconciliation seams: external tax behavior remains invoice- and charge-tax-driven, while canonical recurring service periods stay explanatory context rather than tax allocation or reconciliation inputs
    • packages/billing/src/services/accountingExportService.ts now states the complementary post-export rule inline: automatic external-tax import deduplicates by invoice id and must not fan out one invoice into multiple import attempts just because export lines carry multiple canonical recurring periods
    • server/src/test/unit/accounting/externalTaxConsumers.servicePeriods.test.ts now proves both behaviors directly:
      • reconcileTaxDifferences(...) reads only invoice and invoice_charges tax fields and would fail if it tried to query invoice_charge_details
      • importExternalTaxAfterDelivery(...) imports once per unique invoice even when the export context contains multiple canonical recurring detail-backed lines for the same invoice
    • validation for this checkpoint used cd server && npx vitest run src/test/unit/accounting/externalTaxConsumers.servicePeriods.test.ts --coverage.enabled false plus npx tsc --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) QuickBooks and Xero export adapters now have explicit recurring-period projection policies, which closes F198, F199, T227, and T228:

    • packages/billing/src/adapters/accounting/quickBooksOnlineAdapter.ts now routes line-date selection through an explicit helper: QuickBooks can export only one service date, so canonical recurring ranges pin to the first covered day, end-only fallback lines use the surviving boundary, and financial_document_fallback lines intentionally omit SalesItemLineDetail.ServiceDate
    • server/src/test/unit/accounting/quickBooksOnlineAdapter.spec.ts now proves both sides of that QuickBooks contract: canonical recurring ranges export 2025-01-01 as the service date, while financial-only fallback rows omit it entirely
    • packages/billing/src/adapters/accounting/xeroAdapter.ts now routes service-period shaping through an explicit helper: canonical/header-backed lines keep both compatibility summary bounds, while financial_document_fallback lines stay periodless
    • packages/integrations/src/lib/xero/xeroClientService.ts now flattens Xero line descriptions with explicit date-basis copy instead of raw ISO timestamps: multi-day ranges become Service period: YYYY-MM-DD to YYYY-MM-DD, same-day coverage becomes Service date: YYYY-MM-DD, and null-period lines leave the base description untouched
    • focused coverage now lives in server/src/test/unit/accounting/xeroClientService.spec.ts, server/src/test/unit/accounting/xeroAdapter.spec.ts, and packages/billing/tests/accountingExportAdapters.servicePeriods.wiring.test.ts
    • validation for this checkpoint used focused adapter/unit runs plus npx tsc --noEmit on packages/billing and packages/integrations; the older server/src/test/unit/accounting/xeroAdapter.spec.ts delivery case still expects a live DB-backed mapping write path during deliver(), so the checkpoint relied on the focused transform test rather than claiming the full historical file passes in this local environment
  • (2026-03-17) Reporting and analytics date-basis policy is now explicit across portal, billing, reporting, and finance service seams, which closes F191, F192, F193, F194, T221, T222, T223, and T230:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/REPORTING_DATE_BASIS.md now defines one policy matrix for billing overview, contract revenue, expiration, reconciliation, financial analytics, and service-metric families, including the mixed-cadence rule that financial-operational readers keep invoice / transaction dates while coverage readers keep canonical recurring service periods
    • packages/client-portal/src/actions/client-portal-actions/dashboard.ts now makes the metric split explicit in-source: recent invoice activity may carry recurring coverage summaries, but pending-invoice counts remain invoice-state metrics and must not be silently reinterpreted as service-period totals
    • packages/billing/src/actions/contractReportActions.ts now states the intended reporting split directly where revenue and renewal summaries are assembled: revenue pivots to canonical recurring coverage when detail periods exist, while expiration and decision-due summaries stay assignment-date based
    • packages/reporting/src/actions/reconciliationReportActions.ts and server/src/lib/api/services/FinancialService.ts now document the complementary control rule that reconciliation and financial analytics remain financial-date readers; recurring service periods remain additive explanatory lineage rather than the basis for aging, collections, or discrepancy totals
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks the policy artifact plus those source comments as executable plan contracts, and pass-0-source-inventory.json was refreshed in the same checkpoint so the docs contract continues to match the live persisted-reader inventory
  • (2026-03-17) Financial-artifact fallback handling is now explicit instead of collapsing all missing recurring metadata into the same financial_document_date shape, which closes F190 and T220:

    • packages/billing/src/actions/creditActions.ts now distinguishes three additive invoice-context states for credit readers and transaction history: canonical_recurring, financial_document_fallback, and missing_source_context
    • the intended staged-coexistence rule is now explicit at the reader seam: historical/manual financial artifacts with no canonical recurring detail stay financial_document_fallback, while broken lineage references (for example a transferred credit whose source invoice can no longer be loaded) surface missing_source_context without inventing recurring periods
    • packages/types/src/interfaces/billing.interfaces.ts and server/src/interfaces/billing.interfaces.ts now expose that additive invoice_context_status field on credit-tracking and transaction payloads so dashboard consumers can distinguish legitimate financial-only fallbacks from repair-needed lineage failures
    • packages/billing/src/components/billing-dashboard/CreditManagement.tsx now renders a dedicated Lineage Missing state with repair guidance instead of silently flattening missing-source cases into the same copy used for intentional financial-only artifacts
    • focused executable coverage now lives in server/src/test/unit/billing/creditActions.servicePeriods.test.ts and packages/billing/tests/creditManagement.financialArtifactContext.wiring.test.ts
  • (2026-03-17) Accounting export readers and stored export lines now make recurring-period provenance explicit, which closes F195 and T224:

    • packages/types/src/interfaces/accountingExport.interfaces.ts now defines additive service_period_source metadata with the three supported export-reader states: canonical_detail_periods, invoice_header_fallback, and financial_document_fallback
    • packages/billing/src/services/accountingExportInvoiceSelector.ts now derives that source on preview and batch-selection rows, persists it into accounting_export_lines.payload, and normalizes date-only service-period values so preview and stored export lines agree even when the DB driver returns different Date shapes for DATE columns
    • packages/billing/src/repositories/accountingExportRepository.ts now normalizes reread service_period_start / service_period_end values back onto the shared ISO8601String contract instead of leaking raw connection-specific Date objects to downstream export readers
    • packages/billing/src/services/accountingExportValidation.ts, packages/billing/src/actions/accountingExportActions.ts, server/src/lib/api/controllers/ApiAccountingExportController.ts, and server/src/lib/api/controllers/ApiCSVAccountingController.ts now treat service_period_source as the authoritative projection contract for canonical-detail versus header-fallback versus financial-only export lines
    • focused coverage now lives in server/src/test/unit/accounting/accountingExportInvoiceSelector.servicePeriods.test.ts, server/src/test/integration/accounting/invoiceSelection.integration.test.ts, and packages/billing/tests/accountingExportInvoiceSelector.servicePeriods.wiring.test.ts; the integration seam now explicitly spies AccountingExportService.createForTenant(...) so selector-created batches share the transaction-scoped repository used by the test harness
  • (2026-03-17) Stored export batches now have an explicit mixed-shape reread contract, which closes F196 and T225:

    • packages/billing/src/services/accountingExportService.ts now documents the intended storage rule directly at getBatchWithDetails(...): a stored batch may contain historical/header-fallback lines and canonical-detail-backed recurring lines together, and rereads must preserve each lines stored service_period_source plus summary/detail-period payload instead of collapsing the batch to one inferred timing basis
    • server/src/test/integration/accounting/exportDashboard.integration.test.ts now proves that contract against the real dashboard/service reread seam with one canonical line and one historical fallback line in the same stored batch; both period summaries and per-line payload provenance survive getBatchWithDetails(...)
    • the same integration file now aligns its selector re-export path with the live tenant-aware batch creation seam by spying AccountingExportService.createForTenant(...), keeping the dashboard test harness on the same code path now used by createBatchFromFilters(...)
  • (2026-03-17) Export execution now has an explicit replay/reread immutability rule for recurring-period provenance, which closes F197 and T226:

    • packages/billing/src/services/accountingExportService.ts now states the delivery rule inline at the line-update seam: execute/retry may change transport state such as status and external_document_ref, but they must not rewrite stored service_period_source or recurring-detail payloads
    • server/src/test/unit/accounting/accountingExportService.replay.servicePeriods.test.ts now proves that contract on the focused execution path: validation promotes the batch to ready, delivery moves the line to delivered, a second execute attempt is rejected as invalid state, and the canonical recurring-detail payload survives both the delivery transition and the rejected replay attempt unchanged
    • an attempted DB-backed version of the same assertion in batchLifecycle.integration.test.ts ran into an unrelated @alga-psa/jobs package-entry resolution failure before the suite could load, so this checkpoint uses the focused service-level unit seam instead of broadening unrelated harness repair work
  • (2026-03-17) T100 is still blocked after a fresh recheck against the active local Postgres listener, so it remains open:

    • server/src/test/infrastructure/billing/invoices/prepaymentInvoice.test.ts still spends most of its setup budget replaying older migration-heavy infrastructure bootstrap and then falls over on missing workflow-generator files (scripts/generate-system-email-workflow.cjs and services/workflow-worker/src/workflows/system-email-processing-workflow.ts) before the targeted recurring assertion becomes trustworthy
    • server/src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts still fails during migration bootstrap on 202409071803_initial_schema.cjs with ROLLBACK - Connection terminated unexpectedly, so the negative-invoice half of the integration guard is still a harness problem rather than an application-behavior regression
    • keep defending this area with the focused unit suites already added under creditActions.servicePeriods.test.ts, creditReconciliation.servicePeriods.test.ts, and prepaymentInvoice.periodPolicy.test.ts until the older infrastructure harness is repaired enough to run the real T100 integration seam
  • (2026-03-17) Dashboard and portal invoice views now label financial-only artifacts explicitly instead of silently omitting recurring context, which closes F189 and T339:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.tsx now renders Financial-only line. No recurring service period. for manual or adjustment rows that intentionally lack canonical recurring timing metadata
    • packages/client-portal/src/components/billing/BillingOverviewTab.tsx now surfaces Financial-only invoice... guidance on the next-invoice card when the invoice is manual or credit-affected and no recurring service-period summary is available
    • packages/billing/src/components/billing-dashboard/CreditManagement.tsx now shows a Context column and descriptive copy that distinguish recurring-source credits, transferred recurring credits, and financial-only credits, so dashboard readers can interpret non-service financial artifacts alongside preserved recurring lineage
    • focused coverage now lives in packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx, packages/client-portal/src/components/billing/BillingOverviewTab.servicePeriods.test.tsx, and packages/billing/tests/creditManagement.financialArtifactContext.wiring.test.ts
  • (2026-03-17) Credit transfer readers and audit metadata now preserve recurring lineage instead of dropping it on the target credit, which closes F188 and T219:

    • packages/billing/src/actions/creditActions.ts now resolves direct-or-inherited credit invoice lineage, so listClientCredits(...) and getCreditDetails(...) can keep showing canonical recurring source-invoice context even when a transferred credits own transaction has no direct invoice_id
    • transferCredit(...) now stamps transfer transaction metadata and audit details with the source credit id plus source-invoice date basis and recurring service-period summary, which makes downstream reporting and audit trails explicit for transferred credits that originated from detail-backed recurring invoices
    • the shared/server billing interfaces now expose additive lineage fields (source_credit_id, source_invoice_id, lineage_origin) on both transactions and credit rows
    • server/src/test/unit/billing/creditActions.servicePeriods.test.ts now proves a transferred credit keeps canonical recurring source lineage in both list and detail readers
  • (2026-03-17) Credit expiration and reconciliation now make their date basis explicit, which closes F187 and T218:

    • packages/billing/src/actions/creditReconciliationActions.ts now stamps reconciliation-report metadata with reconciliation_date_basis: financial_document_date, making the control rule explicit: expiration and remaining-amount reconciliation are driven by transaction/credit metadata such as created_at and expiration_date, not by invoice-header or recurring service-period dates
    • the same metadata now preserves recurring lineage additively when available: inconsistent-remaining-amount reports carry the source credit invoices canonical recurring summary plus each application transactions target-invoice date basis and recurring summary fields
    • server/src/test/unit/billing/creditReconciliation.servicePeriods.test.ts now proves both halves of that policy: expired negative-invoice credits reconcile on financial dates, and reconciliation reports for detail-backed credits preserve canonical recurring invoice lineage only as explanatory metadata
  • (2026-03-17) Credit application now distinguishes source-invoice versus applied-to-invoice recurring context, which closes F186 and T217:

    • packages/billing/src/actions/creditActions.ts now preserves the top-level credit summary on the source invoice while separately attaching invoice-number, status, date-basis, and canonical recurring service-period summary fields to each transaction entry returned by getCreditDetails(...)
    • the same action now carries target-invoice context onto CREDIT_NOTE_APPLIED workflow payloads, so credit-application audit streams can tell whether the invoice receiving the credit is a canonical recurring-detail-backed invoice or a purely financial-date artifact
    • shared/workflow/streams/domainEventBuilders/creditNoteEventBuilders.ts, shared/workflow/runtime/schemas/billingEventSchemas.ts, packages/event-schemas/src/schemas/domain/billingEventSchemas.ts, and the shared/server ITransaction interfaces were expanded additively for that target-invoice context
    • focused executable coverage now lives in server/src/test/unit/billing/creditActions.servicePeriods.test.ts, which proves the source credit keeps its original recurring summary while the later credit_application transaction carries the target invoices canonical recurring period metadata
  • (2026-03-17) Credit-note issuance metadata now makes source date basis explicit, which closes F185 and T216:

    • shared/workflow/streams/domainEventBuilders/creditNoteEventBuilders.ts, shared/workflow/runtime/schemas/billingEventSchemas.ts, and packages/event-schemas/src/schemas/domain/billingEventSchemas.ts now carry additive credit-note source metadata: sourceDocumentKind, sourceInvoiceId, sourceInvoiceNumber, sourceInvoiceStatus, sourceInvoiceDateBasis, and optional source recurring service-period summary fields
    • packages/billing/src/actions/creditActions.ts now tags prepayment-issued credits as financial_document_date artifacts and exposes invoice_date_basis alongside source recurring service-period summaries in listClientCredits(...) and getCreditDetails(...)
    • packages/billing/src/actions/invoiceModification.ts now recalculates source invoice summary metadata before publishing CREDIT_NOTE_CREATED for negative invoices, so detail-backed recurring source invoices emit canonical_recurring_service_period while flat or non-service sources fall back to financial_document_date
    • focused executable coverage now lives in server/src/test/unit/billing/creditActions.servicePeriods.test.ts and server/src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts; packages/billing/src/actions/creditActions.ts type narrowing at the transaction reread seam was also tightened, which unblocked npx tsc --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) T100 remains open after this checkpoint even though the underlying integration harness moved forward:

    • server/src/test/infrastructure/billing/invoices/prepaymentInvoice.test.ts and server/src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts now stop hardcoding DB_PORT=5432 and include the auth/secret/db mock exports the current runtime imports require
    • focused integration assertions for canonical recurring detail preservation (T100) and negative-invoice source date-basis lineage (T216) were added to those suites, but local execution is still blocked by older migration-heavy infrastructure harness drift in prepaymentInvoice.test.ts and by migration/test-context instability in negativeInvoiceCredit.test.ts
  • (2026-03-17) Negative-invoice offsets now have an explicit source-period policy, which closes F184 and T215:

    • packages/billing/src/actions/creditActions.ts now documents the offset rule at the source-invoice reader seam: a negative-invoice or prepayment credit keeps its recurring timing context on the source invoice, and later credit-application transactions are financial offsets rather than new service-period definitions
    • server/src/test/unit/billing/creditActions.servicePeriods.test.ts now proves that behavior explicitly for a negative-invoice credit later applied to invoice-2: the credit reader still reports the source invoices canonical recurring period while the transaction history separately records the later financial application
  • (2026-03-17) Prepayment invoices now have an explicit non-service period policy, which closes F183 and T214:

    • packages/billing/src/actions/creditActions.ts now states the intended boundary where prepayment invoices are created: they remain non-service financial artifacts, billing_period_* on the header tracks the immediate financial issuance window, and canonical recurring service periods stay absent
    • server/src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts now proves the live prepayment creation path creates the financial invoice, transaction, and credit-tracking rows without touching invoice_charge_details, which locks the prepayment path away from accidental recurring-period coupling
  • (2026-03-17) Manual-to-recurring provenance is now explicit and advisory-only, which closes F182 and T213:

    • packages/billing/src/services/invoiceService.ts now states the intended provenance rule inline: a manual adjustment or manual percentage discount may point at an existing invoice charge, including a recurring parent charge, but that applies_to_item_id link is advisory manual provenance rather than canonical recurring timing data
    • server/src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts now proves a manual discount targeted at an existing recurring parent charge keeps that linkage and recomputes from the recurring row amount, while still remaining periodless itself
  • (2026-03-17) Manual invoice lines now have an explicit periodless policy contract, which closes F181 and T211:

    • packages/billing/src/services/invoiceService.ts now documents the intended boundary at the persistence seam: manually entered invoice rows remain periodless financial rows and do not create canonical invoice_charge_details or claim recurring service-period truth
    • server/src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts now proves both a manual line and a manual percentage discount persist without service_period_* or billing_timing fields while still calculating their net amounts correctly
  • (2026-03-17) Zero-dollar recurring invoice handling now preserves canonical persistence instead of treating every zero-dollar result like an empty billing window, which closes F180 and T210:

    • packages/billing/src/actions/invoiceGeneration.ts now treats finalAmount === 0 as a dedicated policy seam: empty no-content windows can still be suppressed, but zero-dollar invoices with real charges/discounts/adjustments are still created so recurring detail-backed invoice content is not silently skipped
    • the finalized zero-dollar setting is now live for that path via finalizeInvoiceWithKnex(...), so a zero-dollar recurring invoice can still end the run in a finalized state after canonical persistence occurs
    • focused executable coverage now lives in server/src/test/unit/billing/invoiceGeneration.zeroDollarFinalization.test.ts, while server/src/test/unit/billing/invoiceGeneration.emptyResult.test.ts remains the guard that truly empty no-charge windows still respect the suppress setting
  • (2026-03-17) Percentage-discount recalculation now has an explicit service-period-first contract, which closes F179 and T209:

    • packages/billing/src/services/invoiceService.ts now recalculates manual percentage-discount rows from the current non-discount invoice subtotal or targeted line amount before tax and final totals are recomputed, so recurring detail-backed charges can change the discount base without losing their own canonical service-period provenance
    • the policy is now explicit in code rather than accidental: percentage discount rows stay financial-only rows with no service-period truth of their own, while the recurring lines they discount keep the authoritative service-period metadata
    • focused executable coverage now lives in server/src/test/unit/billing/invoiceService.percentageDiscountRecalculation.test.ts, which proves a recurring line plus manual percentage discount rehydrates to the correct discount amount and final invoice totals
  • (2026-03-17) Billed-through enforcement is now explicitly locked to canonical detail periods instead of invoice-header period fields, which closes F171 and T201:

    • packages/billing/src/lib/billing/billingEngine.ts was already reading duplicate-prevention state from invoice_charge_details.service_period_start, invoice_charge_details.service_period_end, and invoice_charge_details.billing_timing via hasExistingServicePeriodCharge(...); this checkpoint made that lifecycle-enforcement contract explicit rather than leaving it implicit in the helper
    • server/src/test/unit/billing/billingEngine.billedThroughReader.test.ts now proves the reader targets invoice_charge_details as iid plus detail-period fields and does not query billing_period_end from invoice headers, while the existing T087 runtime regression in server/src/test/unit/billing/billingEngine.timing.test.ts remains the behavior-level guard that an already-persisted advance service period is skipped even when the enclosing invoice window metadata is later
    • no production code changed in this checkpoint; the work was to convert the already-migrated billed-through reader into an executable contract before broader recurring lifecycle guards (F172+) build on it
  • (2026-03-17) Client contract-line mutation guards now treat canonical recurring detail periods as authoritative for edit/remove/replace safety checks, which closes F172 and T202:

    • packages/clients/src/actions/clientContractLineActions.ts now reads the latest recurring billed-through boundary from invoice_charge_details.service_period_end joined through contract_line_service_configuration, so contract-line mutation guards follow the same authoritative detail-period source as the recurring billed-through reader
    • historical flat invoices are still protected during staged coexistence because the new helper falls back to the older invoice-header reader only when no canonical recurring detail periods exist yet
    • mutation policy is now explicit in the client action layer: if canonical recurring detail periods already exist, replacing the assigned contract line (contract_line_id swap) is blocked and end-date/deactivation checks compare against the authoritative billed-through service-period end rather than invoice headers
    • focused coverage now lives in server/src/test/unit/billing/clientContractLineMutationGuards.test.ts, proving both the deactivation guard and the replace-after-billing guard
  • (2026-03-17) Renewal and replacement identity is now explicit for superseded recurring lines, which closes F173 and T203:

    • packages/clients/src/actions/clientContractLineActions.ts now states the replacement policy directly in code: once a billed recurring line is superseded, the follow-on line must be created as a fresh contract_lines row so historical recurring detail periods stay attached to the old line identity
    • server/src/lib/api/services/ContractLineService.ts now carries the same rule in the API assignment path, so renewed or replacement assignments are documented as fresh-line operations instead of in-place mutation of a billed line
    • executable coverage now lives in server/src/test/unit/billing/clientContractLineReplacementIdentity.test.ts, which proves the client add-line flow creates a fresh generated line id before cloning template configuration and separately locks the server assign path to the same fresh-identity rule
  • (2026-03-17) Invoice workflow events and audit logs now carry recurring-detail provenance instead of relying implicitly on invoice headers alone, which closes F170 and T199:

    • server/src/lib/api/services/invoiceWorkflowEvents.ts now defines summarizeInvoiceRecurringProvenance(...), which reduces hydrated invoice charges into one additive provenance summary: whether recurring timing is authoritative from canonical detail rows or only parent charge fields, how many detail-backed charges/periods exist, the recurring summary range, and whether timing is uniform or mixed
    • shared/workflow/runtime/schemas/billingEventSchemas.ts now allows that recurringProvenance object on invoice workflow payloads such as sent, status-changed, due-date-changed, overdue, and written-off events, so workflow consumers can distinguish canonical recurring provenance from header-grouping dates without a schema fork
    • server/src/lib/api/services/InvoiceService.ts now hydrates that same provenance summary from InvoiceModel.getInvoiceCharges(...) and attaches it to invoice audit-log details (recurring_provenance) plus the invoice workflow payload builders on invoice update, delete/cancel, finalize, send, payment, credit-application, refund, and bulk-status paths
    • focused executable coverage now lives in server/src/test/unit/invoiceWorkflowEvents.test.ts for the builder/schema side and server/src/test/unit/api/invoiceWorkflowRecurringProvenance.wiring.test.ts for the service-layer audit/workflow wiring seam
  • (2026-03-17) Invoice API schemas now make the legacy-flat versus canonical-detail compatibility contract executable instead of only documenting it, which closes F169 and T198:

    • server/src/lib/api/schemas/invoiceSchemas.ts now accepts both old flat invoice payloads and new detail-backed recurring payloads explicitly: response rows can carry canonical service_period_*, billing_timing, recurring_detail_periods, and recurring_projection, while full invoice responses also accept the deprecated invoice_items alias alongside invoice_charges
    • the same compatibility rule now exists in server/src/lib/api/schemas/financialSchemas.ts, so secondary financial API response validation does not reject canonical recurring detail-backed rows while the main invoice API accepts them
    • the schema contract is intentionally stricter on half-migrated shapes: canonical recurring_detail_periods now require recurring_projection, and detail_period_count must match the hydrated detail array length
    • server/src/test/unit/api/invoiceResponseSchema.compatibility.test.ts now proves three states explicitly: legacy flat invoice payloads still parse through the deprecated alias path, canonical detail-backed payloads parse through the new path, and half-migrated payloads are rejected instead of silently drifting through validation
  • (2026-03-17) Recurring invoice parent/detail projection is now explicit instead of being inferred from whatever summary fields happened to be populated, which closes F161, F162, F163, T191, and T193:

    • packages/types/src/interfaces/invoice.interfaces.ts and server/src/interfaces/invoice.interfaces.ts now define additive recurring_projection metadata on IInvoiceCharge, documenting the stable contract for detail-backed recurring reads: recurring_detail_periods is authoritative, parent service_period_start / service_period_end is the summary range across those rows, and parent billing_timing is only populated when every detail row agrees
    • packages/billing/src/models/invoice.ts now hydrates that projection metadata whenever canonical invoice_charge_details rows exist, so Invoice.getById(...), Invoice.getFullInvoiceById(...), and getInvoiceLineItems(...) all expose the same typed parent/detail semantics instead of an implicit summary-only shape
    • server/src/lib/api/services/InvoiceService.ts now delegates invoice-item reads to the same detail-aware invoice model instead of the legacy invoice_line_items table reader, which keeps API invoice_charges payloads aligned with the canonical recurring read-model contract without changing the outer response shape
    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now closes T191 with a multi-detail recurring parent charge contract, and server/src/test/unit/api/invoiceService.recurringDetailProjection.test.ts closes T193 by proving the API service reuses the same hydrated projection
  • (2026-03-17) Historical flat invoice hydration now has an explicit fallback rule instead of an accidental omission, which closes F164 and T192:

    • packages/types/src/interfaces/invoice.interfaces.ts and server/src/interfaces/invoice.interfaces.ts now state the fallback plainly: when recurring_projection and recurring_detail_periods are absent, readers must preserve any parent-level period fields they already have and must not synthesize canonical detail rows for old flat invoices
    • packages/billing/src/models/invoice.ts now carries an inline guard comment on the no-detail branch so the intended behavior is visible at the exact hydration seam: historical flat invoices remain parent-only when no canonical invoice_charge_details rows exist
    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now closes T192 by proving a historical invoice with no detail rows still hydrates cleanly and intentionally omits synthesized recurring detail metadata
  • (2026-03-17) Preview rows now have an explicit multi-period projection contract instead of only a flattened summary range, which closes F165 and T194:

    • packages/types/src/interfaces/billing.interfaces.ts and server/src/interfaces/billing.interfaces.ts now allow runtime billing charges to carry additive recurringDetailPeriods, giving preview generation one shared shape for charges that summarize one or many canonical recurring periods
    • packages/types/src/lib/invoice-renderer/types.ts now documents the preview-row rule explicitly: servicePeriodStart / servicePeriodEnd is the compatibility summary range, while recurringDetailPeriods is authoritative when present and billingTiming only survives there uniformly
    • packages/billing/src/actions/invoiceGeneration.ts now normalizes preview recurring detail periods from either billing-charge or invoice-charge payloads, derives summary fields from that detail list when needed, and carries recurringDetailPeriods through the preview payload instead of flattening multi-period charges away
    • server/src/test/unit/billing/invoiceGeneration.preview.test.ts now closes T194 by proving one preview charge can summarize two canonical recurring periods while keeping both detail periods visible to renderer-facing consumers
  • (2026-03-17) Rendering adapters now have an explicit flatten-or-expand rule for canonical recurring detail periods, which closes F166 and T195:

    • packages/billing/src/lib/adapters/invoiceAdapters.ts now documents the adapter rule directly in code: rendered rows keep recurringDetailPeriods verbatim when available, flatten servicePeriodStart / servicePeriodEnd to a summary range for one-row template consumers, and flatten mixed timing to null instead of inventing a winner
    • packages/billing/src/lib/adapters/invoiceAdapters.test.ts now closes T195 with a mixed-timing case proving the adapter preserves the canonical detail-period list while exposing a compatibility summary range and a null summary timing
  • (2026-03-17) Client-portal invoice detail projection rules are now explicit for detail-backed, flattened, and omitted recurring period states, which closes F167 and T196:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.tsx now documents the portal policy inline: render the canonical detail-period list when multiple recurring periods exist, flatten to one Service Period line when only one detail or a parent summary range exists, and omit service-period copy entirely when no recurring period metadata is available
    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx now closes T196 by proving the two fallback branches explicitly: a legacy summary-only recurring row renders one flattened service-period label, while a manual adjustment row with no period metadata renders none
  • (2026-03-17) Multi-period recurring ordering and aggregation rules are now explicit instead of relying on upstream input order, which closes F168 and T197:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.tsx now sorts rendered recurring detail periods by canonical start/end before showing them, so client-visible multi-period lists stay deterministic even if an upstream reader returns the periods unsorted
    • packages/client-portal/src/components/billing/recurringServicePeriodSummary.ts now sorts and de-duplicates recurring period labels before aggregating them into overview-style summaries, so invoice-level summary copy stays stable across detail-backed and flattened inputs
    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx now proves the dialog renders unsorted canonical detail periods in chronological order, and packages/client-portal/src/components/billing/recurringServicePeriodSummary.test.ts closes T197 by proving aggregated invoice summaries remain ordered and de-duplicated across mixed detail-backed and summary-backed rows
  • (2026-03-17) Shared invoice-candidate grouping now carries explicit split reasons for PO and financial/export constraints, which closes F158, F159, F160, T189, and T190:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now lets scoped due selections carry PO scope, currency, tax source, and export-shape keys, while scoped candidate groups now expose splitReasons with single_contract, purchase_order_scope, and financial_constraint
    • shared/billingClients/recurringTiming.ts now exports groupDueServicePeriodsForInvoiceCandidates(...), which applies those split constraints within each due window and annotates the resulting candidate groups with the operator-visible reasons they were split
    • server/src/test/unit/billing/recurringTiming.domain.test.ts now locks all three grouping cases: contract-scope splitting, PO-scope splitting, and financial/export splitting with explainability metadata
  • (2026-03-17) The first explicit invoice-split rule is now codified at the shared-domain layer, which closes F157 and T188:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now distinguishes scoped due selections and scoped invoice-candidate groups so grouping logic can carry contract-scope metadata explicitly instead of assuming one invoice window always means one invoice
    • shared/billingClients/recurringTiming.ts now exports groupDueServicePeriodsByInvoiceWindowAndContract(...), which keeps mixed due selections on the same invoice window together only when they also share the same clientContractId
    • server/src/test/unit/billing/recurringTiming.domain.test.ts now proves two due selections on the same invoice window still split into separate candidate groups when their contract scopes differ, preserving the existing single-contract invoice invariant before broader PO/tax/export split rules land
  • (2026-03-17) Client-cadence and contract-cadence due-work selection are now both explicit scheduler inputs instead of implicit billing-cycle-only assumptions, which closes F155, F156, T186, and T187:

    • packages/billing/src/actions/recurringBillingRunActions.ts now maps persisted getAvailableBillingPeriods(...) results into deterministic client-cadence recurring-run targets, selector inputs, and execution windows using the stored period_start_date / period_end_date boundaries rather than any current billing-setting anchor math
    • shared/billingClients/recurringRunExecutionIdentity.ts now exports selectContractCadenceRecurringRunTargets(...), which uses the shared contract-cadence service-period generators plus resolveContractCadenceInvoiceWindowForServicePeriod(...) to produce schedulable contract-owned due windows before invoice grouping occurs
    • server/src/test/unit/billing/recurringBillingRunActions.test.ts now proves client-cadence target selection stays deterministic on historical persisted billing-cycle windows, and server/src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts now proves contract-cadence due-work selection stays deterministic across anniversary-window boundary crossings
  • (2026-03-17) Comparison-mode drift traces now carry deterministic recurring execution identity metadata, which closes F154 and T185:

    • packages/billing/src/actions/invoiceGeneration.ts now routes comparison-mode logging through the selector-input contract and attaches selectionKey, executionIdentityKeys, and executionWindowKinds to legacy-vs-canonical drift warnings instead of logging only a billing-cycle placeholder
    • this keeps comparison-mode safe for staged rollout when recurring selection is no longer defined purely by billingCycleId: contract-cadence or other future execution windows can now be compared with traceable identity metadata even before full scheduler cutover lands
    • server/src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts now proves a contract-cadence selector input emits those deterministic trace fields when comparison mode detects drift
  • (2026-03-17) Recurring-run retries now carry deterministic due-work identity instead of relying on billing-cycle list order, which closes F153 and T184:

    • shared/billingClients/recurringRunExecutionIdentity.ts now derives selectionKey and retryKey from the sorted set of execution-window identity keys, so the same due-work selection yields the same retry scope even if callers pass targets in a different order
    • packages/billing/src/actions/recurringBillingRunActions.ts now includes those deterministic keys in the recurring-run result and workflow payload inputs while still keeping event-bus publish idempotency keyed by the per-attempt runId, which avoids collapsing deliberate retry attempts into the original run record
    • server/src/test/unit/billing/recurringBillingRunActions.test.ts now proves two runs over the same mixed client/contract execution windows produce the same selectionKey and retryKey despite opposite target ordering
  • (2026-03-17) Background recurring selector inputs are now explicit instead of being implicit billing-cycle lookups, which closes F152 and T183:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringDueSelectionInput, and shared/billingClients/recurringRunExecutionIdentity.ts now provides buildBillingCycleDueSelectionInput(...) plus buildContractCadenceDueSelectionInput(...) so background flows can describe the selector window independently from whether invoice persistence still needs a billingCycleId
    • packages/billing/src/actions/invoiceGeneration.ts now exposes calculateBillingForSelectionInput(...), which routes due recurring selection through the new selector-input contract and falls back to the execution-window identity key when no raw billingCycleId exists
    • server/src/lib/jobs/handlers/generateInvoiceHandler.ts and server/src/lib/jobs/index.ts now accept optional selectorInput payloads alongside the execution-window metadata, with a guard that prevents mismatched selector and execution identities from silently diverging in background jobs
    • server/src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts now locks the new contract by proving a contract-cadence selector input can preselect due recurring service periods without any raw billingCycleId in the selector path
  • (2026-03-17) Recurring-run execution identity is now additive and explicit instead of being only a raw billingCycleId, which closes F151, T181, and T182:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts now defines IRecurringRunExecutionWindowIdentity plus explicit billing_cycle_window and contract_cadence_window kinds, so scheduling and workflow metadata can reference a typed recurring execution window instead of a bare string
    • shared/billingClients/recurringRunExecutionIdentity.ts now builds stable execution-window identity keys for both current client billing-cycle windows and future contract-cadence windows, and packages/billing/src/actions/recurringBillingRunActions.ts now normalizes recurring-run targets through that identity layer before emitting workflow metadata
    • server/src/lib/jobs/index.ts and server/src/lib/jobs/handlers/generateInvoiceHandler.ts now carry optional execution-window metadata on invoice-generation jobs while keeping the live execution bridge on billingCycleId; contract-cadence execution windows are therefore schedulable as typed payloads before later selector/job work (F152+, F156+) makes them fully executable
    • focused validation lives in server/src/test/unit/billing/recurringTiming.domain.test.ts, server/src/test/unit/billing/recurringBillingRunActions.test.ts, and server/src/test/unit/billing/recurringBillingRunWorkflowEvents.test.ts; the shared-domain builder test file remains outside the repo's default Vitest include, so the server unit suites are the reliable executable guard for this checkpoint
  • (2026-03-17) Feature-to-subsystem traceability is now explicit in the plan artifacts, which closes F150 and T170:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/FEATURE_SUBSYSTEM_MAP.md now groups the feature backlog into subsystem bands spanning architecture/parity, shared timing domain, runtime execution, invoice generation/persistence, storage/API reconciliation, authoring surfaces, downstream readers/exports, contract cadence, and the later materialized-service-period ledger
    • the map also defines the tracking discipline for future checkpoints: every completed feature should name its primary subsystem, note cross-cut surfaces in SCRATCHPAD.md, and keep commit scope aligned to a coherent subsystem slice instead of only chronological feature order
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now enforces the existence of that mapping artifact and the required subsystem coverage as T170
  • (2026-03-17) Operator investigation guidance now has explicit runbook paths for cadence-owner disputes and header-versus-detail service-period mismatches, which closes F149 and T148:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RUNBOOK.md now includes a dedicated Cadence-Owner Dispute Investigation section that tells operators to compare stored contract_lines.cadence_owner, the active run window, and the persisted invoice_charge_details row before deciding whether a line truly followed client cadence or contract cadence
    • the same runbook now includes a Service-Period Mismatch Investigation section that makes the intended interpretation explicit: invoice headers stay invoice-window grouping dates, while canonical recurring detail rows stay the authoritative coverage dates for migrated recurring lines
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks those operator questions, SQL fragments, and escalation rules as T148, so runbook drift will fail fast instead of silently weakening mixed-cadence support guidance
  • (2026-03-17) Time-and-usage unification now has its own executable follow-on boundary contract, which closes F148 and T169:

    • the time/usage follow-on text was already present in PRD.md and PASS0_RECURRING_TIMING_APPENDIX.md, but server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks that scope boundary explicitly instead of leaving it as prose that could drift
    • this checkpoint did not widen recurring v1 scope; it made the existing rule enforceable: time entries and usage records remain event-driven until a separate follow-on plan deliberately reopens canonical period or ledger semantics for those domains
  • (2026-03-17) Advanced service-period ledger extensions are now explicitly fenced off as follow-on work, which closes F147 and T168:

    • PRD.md and PASS0_RECURRING_TIMING_APPENDIX.md now both carry a dedicated Advanced Service-Period Ledger Extensions boundary that keeps long-range materialization, archival/cold-storage, performance denormalization, and bulk ledger reshaping out of recurring v1 unless the first-cut ledger proves insufficient
    • the new boundary also names the trigger conditions for reopening that scope later: concrete horizon/regeneration cost, read-performance, or retention/storage failures, plus a requirement to define canonical-versus-derived truth, replay/repair, and rollback posture before any ledger extensions land
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks those phrases as T168, and pass-0-source-inventory.json was refreshed at the same time so the existing docs inventory contracts continue to match live rg output
  • (2026-03-17) Post-cutover source-plus-DB validation now proves the migrated live recurring path no longer depends on billing_cycle_alignment or resolveServicePeriod, which closes F146, T166, and T167:

    • server/src/test/unit/billing/billingEngine.cleanupSource.test.ts remains the direct source contract: the billing engine file still does not contain the removed resolveServicePeriod, _calculateProrationFactor, or applyProrationToPlan seams
    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now adds one DB-backed invariance test proving three otherwise-identical partial advance lines with billing_cycle_alignment = start|end|prorated all persist the same canonical service_period_start, service_period_end, and subtotal on the real invoice-generation path
    • the same integration file now adds a DB-backed arrears partial-period test proving a mid-start monthly recurring line still persists 2025-02-10 through 2025-02-28 on the live invoice-generation path after the helper cleanup, which is the real-data closure for the old resolveServicePeriod seam
    • local execution required overriding the stale .env.localtest DB_PORT=5438 setting to DB_PORT=57433, because the active Postgres listener for this worktree is on 57433
  • (2026-03-17) End-exclusive recurring boundary behavior now has explicit regression guards across mixed-cadence selection and pricing-schedule overlap, which closes F145, T149, and T159:

    • server/src/test/unit/billing/billingEngine.timing.test.ts now proves a contract-owned advance line whose first due window starts exactly at the active client invoice-window end is excluded from the current mixed-cadence selection set, so coexistence keeps [start, end) overlap semantics instead of treating touching windows as overlapping
    • server/src/test/unit/billing/billingEngine.discountPricingTiming.test.ts now proves fixed recurring pricing schedules remain end-exclusive on both sides of the canonical service period, excluding schedules that only start at the service-period end or end at the service-period start
    • no production code changed in this checkpoint; the value was locking the already-correct selection/query boundaries in executable tests before later scheduler and reader work adds more mixed-cadence pressure
  • (2026-03-17) Billing dashboard invoice-generation surfaces now explain service-period-first recurring semantics consistently, which closes F139 and adds T332:

    • packages/billing/src/components/billing-dashboard/Overview.tsx now frames invoice management as invoice-window generation for recurring service periods plus separate manual/prepayment financial handling
    • packages/billing/src/components/billing-dashboard/invoicing/GenerateTab.tsx now explains the semantic boundary for automatic, manual, and prepayment flows directly under the invoice-type selector
    • packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx, ManualInvoices.tsx, and PrepaymentInvoices.tsx now each state whether they create recurring service-period coverage, stay periodless, or create credit-only financial artifacts
    • packages/billing/tests/invoicingGenerateTab.recurringCopy.wiring.test.ts was added because F139 previously had no explicit regression item in tests.json; the new T332 tracks this copy/UX contract without overloading unrelated recurring-engine tests
  • (2026-03-17) Obsolete recurring-timing terminology is now cleaned out of migrated live copy and implementation comments, which closes F142 and T165:

    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/FixedFeeServicesStep.tsx, ContractLineDialog.tsx, contracts/CreateCustomContractLineDialog.tsx, contract-lines/FixedContractLineConfiguration.tsx, contract-lines/FixedContractLinePresetConfiguration.tsx, and service-configurations/FixedServiceConfigPanel.tsx now use Adjust for Partial Periods / Partial-Period Adjustment instead of stale Enable Proration / Proration labels on migrated recurring surfaces
    • the remaining readable legacy alignment selector now shows Proportional Coverage instead of the raw Prorated label where staged compatibility still exposes that choice
    • packages/client-portal/src/components/billing/PaymentSuccessContent.tsx and server/src/app/msp/licenses/purchase/success/page.tsx no longer describe recurring invoice outcomes with proration-only or prorated charges copy; both now explain coverage in canonical partial-period language
    • packages/billing/src/actions/contractLineAction.ts and packages/billing/src/lib/billing/billingEngine.ts now describe the remaining compatibility seam as legacy partial-period settings instead of proration-centric comments
    • packages/billing/tests/recurringTimingTerminology.cleanup.test.ts now locks those migrated surfaces against stale recurring-timing wording drifting back in
  • (2026-03-17) Runtime rollout protection now blocks partial mixed-timing states before recurring charge-family execution begins, which closes F143, T150, and T158:

    • packages/billing/src/lib/billing/billingEngine.ts now always builds the canonical recurring timing selection set for the active invoice window and, when callers provide recurringTimingSelections, rejects missing, extra, or divergent line selections with an explicit Recurring timing rollout guard blocked mixed legacy/canonical timing state error
    • this keeps the service-period-first seam honest during staged rollout: action-layer preselection cannot silently cover only some recurring lines while other fixed/product/license work falls back to a different timing path on the same invoice run
    • server/src/test/unit/billing/billingEngine.timing.test.ts now proves both guard modes explicitly: one test covers missing provided selections for one due recurring line in a mixed run, and the other covers a provided selection whose service period diverges from the engine's canonical result for that same line
  • (2026-03-17) Contract wizard draft/resume regression coverage now locks cadence-owner and partial-period defaults through both reread and re-save seams, which closes F144 and adds T337 / T338:

    • packages/billing/tests/draftContractForResumeActions.test.ts now proves getDraftContractForResume(...) returns cadence_owner, enable_proration, and the fixed recurring base rate together for a resumed recurring draft instead of only partially rehydrating those defaults
    • packages/billing/tests/contractWizardResume.test.tsx now drives a resumed fixed-fee wizard back through Save Draft and proves the emitted draft payload keeps the original cadence_owner while preserving a toggled partial-period setting
    • no production code changed in this checkpoint; the draft/resume path was already behaving correctly, but it did not yet have an end-to-end regression guard covering both the action reread and the resumed save-draft emit path
  • (2026-03-17) Template-authored cadence-owner defaults now persist and clone through the real template storage layer, which closes F138, T119, and T120:

    • server/migrations/20260317213000_add_cadence_owner_to_contract_template_lines.cjs adds contract_template_lines.cadence_owner, backfills it from matching contract_lines when possible, and defaults remaining legacy rows to 'client'
    • packages/billing/src/repositories/contractLineRepository.ts plus server/src/lib/repositories/contractLineRepository.ts now read, snapshot, update, and clone template-line cadence_owner instead of dropping it to 'client' during template-to-contract instantiation
    • packages/billing/src/actions/contractWizardActions.ts now lifts cadence_owner from any recurring template/draft line, not only the fixed-fee branch, so hourly-only and usage-only template snapshots and resumed drafts keep their authored cadence defaults
    • adjacent package/server readers that still hardcoded template cadence to 'client' were aligned as part of the same checkpoint: packages/billing/src/actions/contractLineAction.ts, packages/billing/src/actions/contractLineMappingActions.ts, packages/billing/src/models/contractLineMapping.ts, packages/billing/src/models/contractTemplate.ts, and server/src/lib/api/services/ContractLineService.ts
    • focused coverage now lives in server/src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts and packages/billing/tests/templateCadenceOwnerRoundTrip.actions.test.ts, while the existing draft/template cadence-owner suites continue to pass on top of the storage change
  • (2026-03-17) billing_cycle_alignment is now non-executing on the migrated fixed recurring runtime path, which closes F116:

    • packages/billing/src/lib/billing/billingEngine.ts no longer selects, defaults, or propagates contract_lines.billing_cycle_alignment during recurring fixed-charge execution; coverage settlement is now driven only by canonical service-period timing plus enable_proration
    • server/src/test/unit/billing/billingEngine.timing.test.ts closes T162 with an explicit invariance check showing start, end, and prorated legacy values all emit the same canonical fixed recurring charge and no longer appear on the generated charge payload
    • compatibility storage and authoring surfaces intentionally remain in place for later staged-deprecation work (F137+); this checkpoint only removes live execution dependence, not the legacy column itself
  • (2026-03-17) Client-portal invoice preview now preserves canonical recurring detail metadata end to end, which closes T088 without pretending broader portal-reader cutover is done:

    • packages/billing/src/lib/adapters/invoiceAdapters.ts now carries service_period_start, service_period_end, and billing_timing from DB invoice-charge payloads into WasmInvoiceViewModel.items
    • packages/client-portal/src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx proves the portal preview path receives those canonical recurring fields through the real adapter, so preview/pay-style rendering no longer strips them before template rendering
    • this is a preview/read-model seam only; broader dashboard/portal invoice-query cleanup still belongs to F126 and F140
  • (2026-03-17) Recurring invoice preselection now has an explicit mixed-invoice regression guard, which closes T089:

    • server/src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts now proves calculateBillingForInvoiceWindow(...) still returns non-recurring hourly/usage charges untouched while applying canonical recurringTimingSelections only to the recurring timing seam
    • this keeps the service-period-first preselection contract narrow: it chooses due recurring service periods first, but it does not narrow the eventual invoice payload to recurring charges only
  • (2026-03-17) Obsolete recurring timing helpers are now deleted from the live billing engine, which closes F117, T163, and T164:

    • packages/billing/src/lib/billing/billingEngine.ts no longer carries the dead resolveServicePeriod, _calculateProrationFactor, or applyProrationToPlan helpers, and calculateBillingInternal(...) no longer runs a no-op fixed-charge proration pass before combining invoice charges
    • focused timing/product/license tests now assert the old helper is absent rather than spying on it, and server/src/test/unit/billing/billingEngine.cleanupSource.test.ts adds a direct source contract proving the late-stage helper strings are gone from the runtime file
    • the old helper-only regression file server/src/test/unit/billing/billingEngine.prorationExclusive.test.ts was removed because its only purpose was to test a private implementation seam that no longer exists
  • (2026-03-17) Pass-0 implementation artifacts now live in:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PASS0_RECURRING_TIMING_APPENDIX.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/pass-0-source-inventory.json
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts
  • (2026-03-17) Shared recurring-timing primitives now live in:

    • packages/types/src/interfaces/recurringTiming.interfaces.ts
    • shared/billingClients/recurringTiming.ts
    • packages/billing/src/lib/billing/recurringTiming.ts
    • server/src/test/unit/billing/recurringTiming.domain.test.ts
  • (2026-03-17) Client-cadence service-period generation now lives in:

    • shared/billingClients/clientCadenceServicePeriods.ts
    • server/src/test/unit/billing/clientCadenceServicePeriods.domain.test.ts
  • (2026-03-17) The first checkpoint intentionally made the plan executable:

    • source-backed file inventories now cover resolveServicePeriod, billing_cycle_alignment, persisted service-period readers, and downstream recurring timing consumers
    • a docs contract test now fails if the pass-0 appendix drifts from live grep-backed source references
  • (2026-03-17) The second checkpoint intentionally kept the new timing domain additive and pure:

    • canonical service periods and invoice windows are distinct types with explicit kind markers and shared [start, end) semantics
    • cadence-owner defaults are centralized at DEFAULT_CADENCE_OWNER = 'client'
    • activity-window intersection, coverage calculation, due-window mapping, and invoice-detail timing projection are defined without invoice creation side effects
  • (2026-03-17) Client-cadence service-period generation now preserves current anchor behavior while separating two cases explicitly:

    • when no historical cycle is authoritative, generation returns the anchored period containing the requested date
    • when a historical cycle ends exactly at the next cursor, generation treats that cursor as an authoritative transition boundary and moves forward without reaching back into pre-cutover periods
  • (2026-03-17) Shared recurring timing resolution now has one additive seam for the next cutover:

    • selectDueServicePeriodsForInvoiceWindow chooses due service periods from a current invoice window without rebuilding separate advance vs arrears period definitions
    • resolveRecurringSettlementsForInvoiceWindow intersects those due periods with line activity dates, returns covered periods, and computes one coverage ratio for fixed, product, and license families
    • zero-coverage periods are filtered before any family-specific charge math runs, which is the intended bridge away from bespoke proration and skip branches
  • (2026-03-17) The first fixed-recurring runtime cutover now consumes a shared timing helper instead of calling resolveServicePeriod directly:

    • BillingEngine.calculateFixedPriceCharges now resolves due periods via resolveFixedRecurringChargeTiming, which builds canonical current/previous service periods and intersects them with the line activity window
    • an initial bug surfaced immediately in unit tests: subtracting raw day counts broke monthly arrears across month-length changes (2025-02-01 had incorrectly mapped back to 2025-01-04); the helper now subtracts billing-cycle calendar units instead
    • the old arrears-specific skip/clamp branch is gone from the fixed path because empty coverage now returns null before charge generation
    • the advance termination credit branch is still live when fixed-proration remains disabled; that is the next incomplete seam (F044+), not part of this checkpoint
  • (2026-03-17) The next fixed-recurring checkpoint removed the remaining advance-only special case:

    • BillingEngine.calculateFixedPriceCharges no longer emits separate negative advance termination credit rows through buildAdvanceTerminationCredits
    • fixed custom-rate, pricing-schedule override, and FMV allocation paths now flow through one post-generation settlement step when canonical coverage must scale amounts
    • advance final periods without fixed-proration enabled now reuse the canonical coverage ratio with legacy net-of-unused rounding, so the resulting single positive charge matches prior net billing without carrying a synthetic arrears credit row
    • this checkpoint also closed two previously hidden seams in the fixed path: custom-rate override charges and pricing-schedule override charges had been bypassing all shared post-processing after an early return
  • (2026-03-17) Fixed tax behavior is now regression-covered on the canonical timing path:

    • taxable FMV allocations on a partial advance final period now have explicit unit coverage proving net tax amounts are preserved after coverage settlement
    • service-level tax regions remain stable when one service provides an explicit region and another falls back to the client default; both still route through the same fixed recurring settlement path
  • (2026-03-17) Fixed recurring persistence now closes the remaining custom-rate and assignment-metadata seams:

    • custom-rate and pricing-schedule override fixed plans now emit detail-backed fixed charges with config_id, serviceId, client_contract_line_id, and client_contract_id instead of collapsing into a parent-only charge that could not write canonical invoice_charge_details
    • the legacy edge path for fixed-config services on non-Fixed plan types now preserves contract assignment metadata, which keeps PO-scoped grouping and downstream invoice persistence from losing client_contract_id
    • existing fixed-detail persistence tests continue to prove canonical service_period_start, service_period_end, and billing_timing are written without subtotal drift once charges reach persistFixedInvoiceCharges
  • (2026-03-16) packages/billing/src/lib/billing/billingEngine.ts currently mixes several timing models:

    • resolveServicePeriod
    • advance vs arrears branching
    • fixed-fee proration in one path
    • product/license proration later in a different path
  • (2026-03-16) billing_cycle_alignment is surfaced across schema, repositories, APIs, and UI, but the execution model is not cleanly organized around it. This makes it a strong cleanup target once service periods are explicit.

  • (2026-03-16) Current invoice timing tests are effectively asserting service-period behavior already, especially in server/src/test/integration/billingInvoiceTiming.integration.test.ts.

  • (2026-03-16) The recurring-billing mental model shown to users today still leans on “enable proration for mid-month starts,” which is a symptom of implicit rather than canonical service periods.

  • (2026-03-17) The blast radius is substantially wider than the first draft plan represented. Additional impacted surfaces identified during replan:

    • invoice generation and recurring billing runs
    • invoice detail persistence and billed-through calculations
    • credits, prepayment, and negative-invoice flows
    • purchase-order and pricing-schedule dependencies
    • accounting exports and service-period consumers
    • client portal invoice/plan details
    • reporting actions/definitions
    • repositories/models/schemas/test helpers
  • (2026-03-17) Second-pass agent findings tightened the highest-risk seams:

    • billingCycleId is still the true execution identity in recurring runs and job handlers
    • invoice read models still hydrate mostly parent invoice_charges, not canonical detail rows
    • export preview still derives service periods from invoice headers in some paths
    • repository and model write paths normalize or silently drop timing fields inconsistently
    • template and preset authoring paths are still not guaranteed to propagate future cadence semantics
  • (2026-03-17) Materialization adds a new class of risk the plan must own explicitly:

    • generation horizon
    • override provenance
    • billed-period locking
    • regeneration after contract edits
    • future-period edit operations and conflict handling
  • (2026-03-17) The plan should explicitly avoid silently dragging time/usage into v1, while still specifying their compatibility boundaries and non-goals.

  • (2026-03-17) Resolve the v1 cadence_owner persistence question in favor of contract_lines.cadence_owner.

    • Rationale: live recurring timing is already line-scoped on contract_lines (billing_frequency, billing_timing), while client_contracts only provides assignment windows and template lines remain a later authoring/defaults surface.
  • (2026-03-17) Fixed recurring metadata preservation now has an explicit persistence regression guard:

    • packages/billing/src/services/invoiceService.ts was already carrying client_contract_id on grouped detailed fixed charges, but the consolidated parent invoice_charges insert dropped that assignment field entirely
    • server/src/test/unit/billing/invoiceService.fixedPersistence.test.ts now reproduces the issue without a database and verifies that canonical invoice_charge_details periods and invoice_charge_fixed_details.allocated_amount stay aligned with the parent fixed charge subtotal
    • packages/billing/src/services/invoiceService.ts now persists client_contract_id on consolidated fixed parent rows, preserving assignment metadata for downstream invoice readers and PO-related invoice flows
    • server/src/test/unit/billing/billingEngine.timing.test.ts now adds explicit fixed-recurring parity coverage for full-period, mid-start, and mid-end monthly client-cadence scenarios
    • server/src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts now includes a PO + fixed-detail persistence regression test, but local execution remains blocked by the current Postgres socket permissions
  • (2026-03-17) Bucket overage timing now maps onto explicit allowance periods instead of invoice windows:

    • packages/billing/src/lib/billing/billingEngine.ts now groups bucket_usage rows by period_start + period_end and emits one bucket charge per explicit usage period
    • bucket charge servicePeriodStart and servicePeriodEnd now follow the persisted bucket period boundaries, which is the first concrete definition for F051
    • tax-date evaluation for bucket overages now uses the allowance period end date instead of the enclosing invoice window end, which keeps the charge aligned with the bucket period it represents
    • server/src/test/unit/billingEngine.test.ts now covers multi-period bucket usage inside one invoice window and asserts that the emitted bucket charges preserve the two distinct service periods
  • (2026-03-17) Bucket rollover calculation now follows the same client-cadence boundary source in both writer paths:

    • packages/billing/src/services/bucketUsageService.ts now prefers client_billing_cycles for the active allowance period and, when rollover is enabled, uses the previous client billing cycle before falling back to anchor math
    • packages/scheduling/src/services/bucketUsageService.ts now mirrors that same client-cycle-aware rollover logic so time-entry writes and billing writes cannot materialize different bucket_usage periods for the same recurring obligation
    • server/src/test/unit/billing/bucketUsageService.periods.test.ts and packages/scheduling/tests/bucketUsageService.periods.test.ts now lock the regression with a monthly client-cadence rollover scenario that would previously have reached back to the contract start anchor instead of the prior billing cycle
  • (2026-03-17) Bucket overage invoice-window attachment is now explicitly covered on the service-period-first path:

    • packages/billing/src/lib/billing/billingEngine.ts continues to select bucket_usage rows by the active billing window, but the focused regression now proves that the emitted charge keeps the persisted allowance period instead of collapsing back to invoice-window dates
    • server/src/test/unit/billing/billingEngine.bucketTiming.test.ts now covers both the explicit service-period mapping and the invoice-window filter that keeps February allowance usage on the February invoice window instead of rebilling it elsewhere
  • (2026-03-17) Fixed recurring pricing schedules now evaluate against the canonical due service period, not the enclosing invoice window:

    • packages/billing/src/lib/billing/billingEngine.ts now resolves the due service-period [start, end) boundaries first and uses those exclusive boundaries when selecting contract_pricing_schedules for fixed recurring charges
    • server/src/test/unit/billing/billingEngine.discountPricingTiming.test.ts proves the intended arrears behavior explicitly: a January schedule still wins for a February invoice window when that invoice is billing the January service period, while a schedule starting exactly on February 1 is excluded from that January service period
  • (2026-03-17) Discount applicability now follows canonical recurring service periods when charge timing has already migrated:

    • packages/billing/src/lib/billing/billingEngine.ts now builds per-contract-line discount evaluation windows from emitted charge servicePeriodStart/servicePeriodEnd values and uses those windows to filter discount overlap in-memory after a broader candidate query
    • the query still falls back to invoice-window overlap when no canonical charge service period exists for a contract line, which keeps non-migrated or non-recurring paths on current behavior
    • server/src/test/unit/billing/billingEngine.discountPricingTiming.test.ts now proves the intended arrears case explicitly: a January discount applies to the February invoice window when the fixed recurring line is billing the January service period, while February/March discounts do not
  • (2026-03-17) Fixed recurring PO association now has an executable unit guard in addition to the blocked DB integration:

    • server/src/test/unit/billing/invoiceService.fixedPersistence.test.ts now proves that detail-backed fixed recurring persistence keeps client_contract_id on the consolidated parent invoice line, which is the assignment linkage PO-scoped readers and invoice selection rely on
    • the existing DB-backed regression in server/src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts remains valuable, but local execution is still blocked by current Postgres permissions
  • (2026-03-17) Fixed recurring tax-date evaluation now follows the canonical service period instead of the enclosing invoice window:

    • packages/billing/src/lib/billing/billingEngine.ts now passes servicePeriodEnd into fixed recurring tax calculations for both the main FMV-allocation path and the legacy fixed-service edge path
    • server/src/test/unit/billing/billingEngine.timing.test.ts now asserts the tax service sees 2025-01-20 for a partial advance final period instead of the enclosing invoice window end, which closes F056
  • (2026-03-17) Negative recurring fixed totals and credit-generating allocations are now explicit, not implied by an inline comment:

    • server/src/test/unit/billing/billingEngine.timing.test.ts now proves a net-negative fixed recurring charge keeps its canonical service period metadata and that a mixed positive/negative FMV allocation can emit one negative detailed charge without breaking the settled period boundaries
    • no runtime code change was needed for F057; the checkpoint was to turn previously implicit support into an executable contract before product/license migration reuses the same settlement model
  • (2026-03-17) The out-of-scope boundary for time, usage, and non-recurring bucket readers is now explicit in the plan artifacts:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PASS0_RECURRING_TIMING_APPENDIX.md now spells out exactly which time-entry and usage-record behaviors stay event-driven in v1, and which bucket behaviors are in scope versus still deferred
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now enforces those explicit phrases so T060 fails if the appendix drifts back toward vague scope language
  • (2026-03-17) A follow-up attempt to close F058 uncovered a local-environment blocker rather than a product behavior decision:

    • a targeted prepaymentInvoice.test.ts regression was drafted to prove prepayment credit application does not strip canonical recurring detail rows, but local execution is blocked by ECONNREFUSED to Postgres on 127.0.0.1:5432
    • that regression was intentionally not left in the tree or marked complete; F058 remains open until the DB-backed path can be executed and verified
  • (2026-03-17) Prepayment-applied recurring invoice rereads now preserve canonical service-period metadata without waiting on the blocked DB path:

    • packages/billing/src/models/invoice.ts now reattaches service_period_start, service_period_end, and billing_timing from invoice_charge_details when invoice charges are reread, so applying prepayment credit no longer collapses migrated recurring timing back to parent-only invoice rows
    • packages/types/src/interfaces/invoice.interfaces.ts and server/src/interfaces/invoice.interfaces.ts now expose those recurring detail fields on IInvoiceCharge, which is the minimum shared contract needed for prepayment-applied invoice readers
    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts closes T096 and T097 with source-level reread coverage for one and multiple prepayment-applied recurring cycles
    • the DB-backed end-to-end prepayment flow is still unavailable locally because Postgres is not listening on 127.0.0.1:5432; T100 remains open for the broader integration seam
  • (2026-03-17) Manual invoice create/update flows now reread through the same canonical invoice reader contract used by recurring invoices:

    • packages/billing/src/actions/manualInvoiceActions.ts now returns Invoice.getFullInvoiceById(...) after manual invoice creation and update instead of hand-building invoice_charges directly from invoice_charges rows
    • this keeps manual flows on the same post-persist projection path as recurring invoices while still leaving manual lines periodless because persistManualInvoiceCharges(...) does not create invoice_charge_details
    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now closes T075 with a mixed recurring-plus-manual reread contract proving canonical service periods attach only to recurring detail-backed lines and do not leak onto manual adjustments
    • a targeted manualInvoice.test.ts integration slice was attempted for this checkpoint, but the suite currently aborts before execution because its shared @alga-psa/auth mock omits withAuth; the checkpoint is therefore defended by the focused unit reread test plus a package TypeScript compile
  • (2026-03-17) Purchase-order follow-up is still open after recon:

    • server/src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx is stale against AutomaticInvoices, which now calls generateInvoicesAsRecurringBillingRun instead of the older generateInvoice action the test spies on
    • integration coverage for PO + recurring detail persistence already exists in server/src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts, but local execution is still blocked by unavailable Postgres
    • that leaves F059 implementable, but not as the next low-risk checkpoint; product-path inventory (F061) is cleaner to land first
  • (2026-03-17) Purchase-order recurring safeguards are now regression-covered on the current UI execution path:

    • server/src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx now imports AutomaticInvoices directly, mocks generateInvoicesAsRecurringBillingRun, and proves PO overage confirmations still gate batch and preview generation on the recurring-run path rather than the older per-cycle generateInvoice action
    • server/src/test/unit/billing/contractPurchaseOrderSupport.poBanner.ui.test.tsx now imports PurchaseOrderSummaryBanner directly so the banner contract stays executable without pulling unrelated package-entry dependencies into the test graph
    • no runtime code change was needed for F059; the recurring PO behavior and PO banner rendering were already correct, but the stale tests were no longer proving it after the recurring-run action cutover
    • T099 remains open because the broader credits-plus-PO interaction still depends on DB-backed coverage outside this UI regression seam
  • (2026-03-17) Purchase-order consumption and selection now stay stable when recurring invoices carry applied credits, which closes F131:

    • packages/billing/src/services/purchaseOrderService.ts#getPurchaseOrderConsumedCents(...) now sums finalized PO consumption as total_amount - credit_applied instead of raw total_amount, so recurring invoices that are partially or fully settled by credits no longer overstate PO usage during overage checks
    • aggregate PO consumption now floors at zero after summing net invoice amounts, which preserves the existing behavior that negative-credit corrections can reduce prior PO consumption but cannot drive the contract below zero consumed cents overall
    • packages/billing/tests/purchaseOrderService.creditConsumption.test.ts now proves both sides of that contract: credit-applied recurring invoices reduce consumed PO cents before overage checks, and aggregate consumption cannot go negative when credits outweigh prior billed totals
    • the existing recurring-run PO UI regressions in server/src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx and server/src/test/unit/billing/contractPurchaseOrderSupport.poBanner.ui.test.tsx remain green on top of the new service behavior, which is the executable closure for T099
  • (2026-03-17) Tax-source and external-tax consumers are now explicitly guarded against recurring service-period drift, which closes F132:

    • packages/billing/src/services/invoiceService.ts now carries explicit comments on the external and pending_external tax branches stating that imported tax remains amount-authoritative even when recurring detail periods are present elsewhere on the invoice
    • packages/billing/src/actions/taxSourceActions.ts now makes the complementary rule explicit for finalization: tax gating follows import state (tax_source) rather than recurring service-period metadata
    • focused regression coverage now lives in packages/billing/tests/invoiceService.externalTax.servicePeriods.wiring.test.ts and packages/billing/tests/taxSourceActions.servicePeriods.wiring.test.ts, which lock those tax/import/reconciliation seams away from accidental service-period coupling
  • (2026-03-17) Recurring product timing now has an explicit migration target:

    • packages/billing/src/lib/billing/billingEngine.ts still constructs product charges against the enclosing invoice window, stamps servicePeriodStart/servicePeriodEnd from billingPeriod.startDate / billingPeriod.endDate, and calculates initial tax from billingPeriod.endDate
    • BillingEngine.calculateBilling still runs applyProrationToPlan(...) on productCharges after the charges are built when enable_proration is true
    • PASS0_RECURRING_TIMING_APPENDIX.md, pass-0-source-inventory.json, and server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now treat that combination as the source-backed F061/T061 seam to remove during F062+
  • (2026-03-17) Recurring product charges now follow canonical service-period timing under client cadence:

    • packages/billing/src/lib/billing/billingEngine.ts now resolves product due periods through the same shared recurring timing helper used by fixed recurring charges, so product servicePeriodStart / servicePeriodEnd now reflect the canonical due service period instead of the enclosing invoice window
    • product coverage settlement now happens inside calculateProductCharges(...), which removes the late-stage applyProrationToPlan(...) branch for products from calculateBilling while preserving the existing enable_proration gate and rounding behavior
    • product tax evaluation now uses the canonical due service-period end date, and product charge tests now explicitly cover client-cadence timing (T062), override precedence (T063), and tax-date behavior (T064)
  • (2026-03-17) Recurring license timing is still a source-backed inventory seam rather than a real runtime migration:

    • packages/billing/src/lib/billing/billingEngine.ts still leaves licenses on the late-stage applyProrationToPlan(...) branch in calculateBilling
    • calculateLicenseCharges(...) still contains the unresolved license-source TODO and, if populated, would still stamp period_start, period_end, servicePeriodStart, and servicePeriodEnd from the enclosing invoice window while evaluating tax from billingPeriod.endDate
    • PASS0_RECURRING_TIMING_APPENDIX.md, pass-0-source-inventory.json, and server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now treat that as the explicit F065/T065 seam to remove before F066+
  • (2026-03-17) Recurring license timing now follows canonical service-period timing under client cadence:

    • packages/billing/src/lib/billing/billingEngine.ts now routes both product and license recurring charges through one shared quantity-charge helper that starts from resolveRecurringChargeTiming(...), so licenses no longer depend on the late-stage applyProrationToPlan(...) branch in calculateBilling
    • license selection is now explicit and source-backed: recurring products exclude service_catalog.is_license = true, while recurring licenses require service_catalog.item_kind = 'product' plus is_license = true, which closes the earlier placeholder TODO without changing contract-line authoring storage
    • license coverage settlement now uses applyQuantityChargeCoverageSettlement(...) inside the canonical timing path, and license tax now evaluates from the canonical due service-period end date instead of the enclosing invoice window
    • server/src/test/unit/billing/billingEngine.licenseTiming.test.ts now locks T066, T067, and T068, and the pass-0 appendix/inventory contract test was refreshed so the plan artifacts describe the migrated product/license timing status instead of the old placeholder seam
  • (2026-03-17) Product and license persistence now keep canonical detail metadata without forcing fixed-detail tax semantics onto non-fixed charges:

    • packages/billing/src/services/invoiceService.ts now inserts one invoice_charge_details row for each recurring product or license parent charge, preserving service_period_start, service_period_end, and billing_timing for non-fixed recurring lines
    • calculateAndDistributeTax(...) now treats only parents with matching invoice_charge_fixed_details rows as consolidated tax carriers, so product/license detail metadata can exist without making tax distribution skip their parent invoice rows
    • server/src/test/unit/billing/invoiceService.fixedPersistence.test.ts now locks T069 and T070 alongside the existing fixed-detail regression coverage
  • (2026-03-17) The next boundary after F070 is no longer a local billing-engine seam:

    • packages/billing/src/actions/invoiceGeneration.ts still derives work from billing_cycle_id, reads one billing cycle window, and calls BillingEngine.calculateBilling(client_id, cycleStart, cycleEnd, billing_cycle_id) directly
    • packages/billing/src/actions/recurringBillingRunActions.ts still treats a recurring run as a list of raw billing-cycle IDs, so F071+ will need broader action/scheduler identity changes rather than another contained engine-only patch
  • (2026-03-17) Invoice generation now has an explicit due-service-period preselection seam before charge-family execution:

    • packages/billing/src/lib/billing/billingEngine.ts now supports an additive recurringTimingSelections option on calculateBilling(...) and exposes selectDueRecurringServicePeriodsForBillingWindow(...), which precomputes one canonical timing selection per recurring contract line from the current billing window before fixed, product, and license charge paths run
    • calculateBillingInternal(...) now builds those selections once per invoice window and threads them into the recurring charge families, so invoice generation no longer relies on each family rediscovering due periods independently inside its own path
    • packages/billing/src/actions/invoiceGeneration.ts now routes preview, PO-overage inspection, and invoice generation through calculateBillingForInvoiceWindow(...), making the action-layer contract explicit: select due recurring service periods first, then calculate billing with those selections
    • direct unit tests that call private recurring helpers still work because the migrated fixed/product/license helpers now fall back to getBillingCycle(...) when no preselected billing cycle is provided; that preserves old test harness signatures while keeping live execution on the new preselection seam
  • (2026-03-17) Invoice headers and downstream invoice-generation hooks remain anchored to the invoice window after recurring details move onto canonical service periods:

    • packages/billing/src/actions/invoiceGeneration.ts already persisted billing_period_start / billing_period_end from cycleStart / cycleEnd; server/src/test/unit/billing/invoiceGeneration.headerPeriods.test.ts now turns that into an executable contract against a recurring charge whose canonical servicePeriodStart / servicePeriodEnd are different
    • the same unit contract now proves createInvoiceFromBillingResult(...) still drives the existing downstream draft/finalization path: persisted-charge subtotal calculation, tax distribution, invoice total update, and invoice-generated analytics all continue to run on the service-period-first path without reinterpreting the invoice header dates from recurring detail periods
    • the test harness surfaced one subtle implementation detail that is now explicit in the regression: invoice header billing-period fields are stored as Temporal.PlainDate values before insert, not raw strings, but they still stringify back to the invoice-window dates exactly
  • (2026-03-17) Empty recurring due-work selection now has an explicit action-layer contract:

    • server/src/test/unit/billing/invoiceGeneration.emptyResult.test.ts closes T077 by proving generateInvoice(...) returns null when selectDueRecurringServicePeriodsForBillingWindow(...) yields no due recurring work, calculateBilling(...) returns zero charges / zero final amount, and zero-dollar suppression is enabled
    • the regression also proves the new due-period preselection seam still receives the expected invoice window bounds before the action exits on the empty-result path, which keeps F071 and the future duplicate/billed-through work (F080) grounded in one explicit contract
  • (2026-03-17) Automatic recurring runs now have an executable delegation contract for the service-period-first path:

    • server/src/test/unit/billing/recurringBillingRunActions.test.ts proves generateInvoicesAsRecurringBillingRun(...) still fans out each selected billingCycleId through generateInvoice(...), which is now the action path that performs due-service-period preselection before billing calculation
    • the test also keeps the recurring-run workflow events stable on that migrated path, so automatic invoice generation remains wired through the same started/completed event contract while recurring timing semantics move underneath generateInvoice(...)
    • no runtime code change was required for F074; the new seam landed in generateInvoice(...) during F071, and this checkpoint locks the automatic-run delegation onto that migrated action path
  • (2026-03-17) Preview generation now has an executable service-period-first contract without widening the preview payload shape yet:

    • server/src/test/unit/billing/invoiceGeneration.preview.test.ts proves previewInvoice(...) constructs a BillingEngine preview through calculateBillingForInvoiceWindow(...), so preview generation now preselects due recurring service periods before calculating recurring charges
    • the preview regression intentionally keeps the current payload surface stable: preview totals and line items remain the same, but the underlying recurring charge source is now canonical service-period selection rather than per-family ad hoc timing math
    • the "generate from preview" UI path already rides generateInvoicesAsRecurringBillingRun(...), so F074 plus the existing preview-dialog UI checks are the current guard that single-cycle generation stays on the same migrated execution path
  • (2026-03-17) The pass-0 source inventory needed a maintenance refresh after the last billing-engine/unit-test checkpoints:

    • pass-0-source-inventory.json now includes the new billing_cycle_alignment reference in server/src/test/unit/billing/billingEngine.discountPricingTiming.test.ts
    • the persisted-service-period reader inventory now also includes server/src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts, server/src/test/unit/billing/billingEngine.bucketTiming.test.ts, server/src/test/unit/billing/billingEngine.discountPricingTiming.test.ts, and server/src/test/unit/billing/invoiceService.fixedPersistence.test.ts
  • (2026-03-17) Negative-invoice and credit-application recurring safeguards are now locked as executable contracts without needing another runtime patch:

    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now closes T091, T094, and T095 by proving partial credit-applied recurring invoices, negative recurring invoices, and negative-source-plus-positive-target rereads all keep canonical invoice_charge_details service periods attached to recurring lines
    • server/src/test/unit/billing/billingEngine.timing.test.ts now closes T078 by proving advance duplicate-prevention checks use the canonical service-period end date (2025-01-31) instead of the enclosing invoice-window end (2025-02-01), which is the billed-through seam F080 depends on
    • no production code changed for F077; the existing runtime behavior already preserved these paths, but the plan had no focused regression contract proving it after the service-period-first cutover
    • a targeted DB-backed run of negativeInvoiceCredit.test.ts is still blocked locally because its suite-level @alga-psa/core/secrets mock no longer exports getSecret, so this checkpoint uses focused unit coverage instead of widening that unrelated harness repair
  • (2026-03-17) Credit expiration and reconciliation now have focused recurring regression coverage, but F078 is still not fully closed:

    • server/src/test/unit/billing/creditReconciliation.servicePeriods.test.ts now closes T092 and T093 by proving two core invariants on the service-period-first path:
      • a negative-invoice credit that is fully consumed by a later credit application still reconciles to remaining_amount = 0 without any dependency on invoice-header timing
      • an expired negative-invoice credit still expires from transactions.expiration_date / credit_tracking.expiration_date, marks the tracking row expired, and creates the same reconciliation-report signal when the client balance has not yet been corrected
    • this checkpoint intentionally stops short of flipping F078 because the broader credit-reader (T098) and DB-backed integration (T100) seams are still open
  • (2026-03-17) Contract wizard cadence-owner capture now reaches the live recurring create/resume UI instead of stopping in action-layer types:

    • packages/billing/src/components/billing-dashboard/contracts/ContractWizard.tsx now carries cadence_owner in ContractWizardData, defaults new wizards to 'client', preserves the value when template snapshots are applied, and includes it in buildSubmissionData()
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/FixedFeeServicesStep.tsx now renders the same business-language cadence-owner chooser used by the fixed contract-line editor, with the contract-anniversary option visibly staged but disabled during rollout
    • packages/billing/tests/contractWizardResume.test.tsx now proves both sides of the wizard contract: new recurring lines submit cadence_owner: 'client', and resumed draft flows keep a stored cadence_owner value across step transitions
    • packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts now locks the source-level seams so future edits cannot silently drop cadence owner from wizard defaults, template snapshot application, or fixed-fee UI copy
  • (2026-03-17) Template-authored recurring defaults now carry cadence owner instead of silently dropping it between wizard state and stored template lines:

    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/TemplateWizard.tsx now defaults template wizard state to cadence_owner: 'client', keeps that value through reset paths, and submits it in buildSubmissionData()
    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/steps/TemplateFixedFeeServicesStep.tsx now renders the staged cadence-owner chooser in business language so template authors see the same client-schedule versus contract-anniversary framing as live contract authors
    • packages/billing/src/actions/contractWizardActions.ts now persists cadence_owner: submission.cadence_owner ?? 'client' on all template-authored contract lines created by the wizard, which keeps future template snapshot and clone flows from losing the default at write time
    • packages/billing/tests/templateWizardBucketOverlay.test.ts and packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts now prove the default submits as 'client' and that the UI/state/write seams stay source-backed
  • (2026-03-17) Fixed recurring authoring copy now explains partial-period coverage without teaching proration as the primary timing workaround:

    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/FixedFeeServicesStep.tsx, packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLineConfiguration.tsx, packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLinePresetConfiguration.tsx, packages/billing/src/components/billing-dashboard/ContractLineDialog.tsx, and packages/billing/src/components/billing-dashboard/contracts/CreateCustomContractLineDialog.tsx now describe enable_proration as scaling a recurring fee to covered service-period time instead of as a mid-cycle workaround
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/ReviewContractStep.tsx now reflects that wording back to the user as Partial-Period Adjustment
    • packages/billing/src/components/billing-dashboard/service-configurations/FixedServiceConfigPanel.tsx now explains billing-cycle alignment as a legacy coverage-calculation choice rather than a generic proration toggle
    • packages/billing/src/components/billing-dashboard/Overview.tsx, packages/billing/src/components/billing-dashboard/contracts/QuickStartGuide.tsx, and packages/billing/src/components/billing-dashboard/BillingCycles.tsx now describe client billing schedules, invoice windows, and service periods in service-period-first language
    • packages/billing/tests/fixedRecurringPartialPeriodCopy.wiring.test.ts and packages/billing/tests/billingDashboardRecurringCopy.wiring.test.ts lock those phrases so the old “enable proration for mid-month starts” guidance does not drift back in unnoticed
  • (2026-03-17) Cadence-owner persistence is now explicit in live code, not only in domain types:

    • server/migrations/20260317170000_add_cadence_owner_to_contract_lines.cjs adds contract_lines.cadence_owner with default/backfill to 'client' plus a check constraint for client|contract
    • packages/types/src/interfaces/billing.interfaces.ts, packages/types/src/interfaces/contract.interfaces.ts, server/src/interfaces/billing.interfaces.ts, and server/src/interfaces/contract.interfaces.ts now expose cadence_owner on live contract-line and mapping interfaces
    • server/src/lib/repositories/contractLineRepository.ts, shared/billingClients/contractLines.ts, packages/clients/src/actions/clientContractLineActions.ts, packages/billing/src/actions/contractLineAction.ts, and packages/billing/src/lib/billing/billingEngine.ts now read/write cadence_owner from contract_lines and default missing legacy rows to 'client'
    • template lines were intentionally left out of the new schema in this checkpoint; repository reads still default template-backed responses to 'client', but persistence on template authoring remains follow-on work (F093+, F138+, F212+)
  • (2026-03-17) Server API cadence-owner handling now matches the live contract-line storage decision:

    • server/src/lib/api/schemas/contractLineSchemas.ts and server/src/lib/api/schemas/financialSchemas.ts now accept cadence_owner only as client|contract on contract-line and client-contract-line request surfaces and require a valid cadence owner on line response schemas
    • server/src/lib/api/services/ContractLineService.ts now defaults create/get/update responses to 'client' when legacy rows lack the field, while still persisting explicit cadence_owner values on live contract-line writes
    • template/billing-settings API surfaces were intentionally not widened in this checkpoint because v1 persistence is still line-scoped on contract_lines; later authoring/default surfaces stay queued behind F086+ and F121+
  • (2026-03-17) Billing-package authoring actions now keep live cadence-owner semantics on the remaining contract-line creation paths:

    • packages/billing/src/actions/contractWizardActions.ts now threads submission.cadence_owner ?? 'client' into all four live recurring line creates inside createClientContractFromWizard(...), and getDraftContractForResume(...) / getContractTemplateSnapshotForClientWizard(...) now surface the fixed-line cadence owner back to the UI while defaulting missing legacy values to 'client'
    • packages/billing/src/actions/contractLinePresetActions.ts now accepts cadence_owner on CreateCustomContractLineInput and preset-copy overrides, so custom recurring lines and preset-instantiated live lines no longer drop cadence ownership at the action layer
    • template authoring storage remains intentionally out of scope for this checkpoint; template snapshot reads expose cadence owner for compatibility, but no template write path claims persistence until the later template-default features land (F093+, F138+, F212+)
    • packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts, packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts, and packages/billing/tests/draftContractForResumeActions.test.ts now close T106 with a mix of action execution and focused wiring/readback coverage
    • the existing draft-resume permission test was also corrected to assert the action's real permissionError(...) return shape instead of expecting a thrown exception; that was harness drift, not a product behavior change
  • (2026-03-17) Recurring timing fixtures now have one shared cadence-owner-aware test seam, which closes F087:

    • server/src/test/test-utils/recurringTimingFixtures.ts now exports shared recurring obligation, service-period, invoice-window, and monthly recurring fixture builders with stable defaults for cadenceOwner, duePosition, and charge family
    • server/src/test/unit/billing/recurringTiming.domain.test.ts now consumes those shared builders and closes T107 with an explicit client-vs-contract cadence fixture contract instead of inline one-off setup
    • server/test-utils/billingTestHelpers.ts now accepts cadenceOwner on createFixedPlanAssignment(...) and persists it to contract_lines.cadence_owner, so DB-backed invoice and credit fixtures can opt into contract-owned cadence later without rewriting helper internals
    • server/src/test/unit/billing/billingTestHelpers.recurringFixtures.test.ts locks that DB-helper seam with a focused source contract so cadence owner cannot silently fall back out of shared fixture setup
  • (2026-03-17) Staged-rollout validation now blocks contract cadence writes before contract-owned runtime support exists, which closes F088:

    • shared/billingClients/cadenceOwnerRollout.ts now defines one shared rollout guard message and helper so schema and service layers reject the same unsupported cadence-owner state consistently
    • server/src/lib/api/schemas/contractLineSchemas.ts and server/src/lib/api/schemas/financialSchemas.ts now reject authoring payloads that set cadence_owner: 'contract', while still allowing stored/read-model response payloads to expose cadence_owner: 'contract'
    • server/src/lib/api/services/ContractLineService.ts now enforces the same staged-rollout guard on create/update paths, so API callers cannot bypass schema validation and persist contract cadence early
    • this keeps the persistence/model plumbing from F081-F087 intact for later rollout phases while making the current rollout posture explicit: client cadence is the only supported live write path until contract-cadence execution exists
  • (2026-03-17) Legacy cadence-owner compatibility is now explicit across package, client-action, and server-service read/write seams, which closes F089:

    • packages/billing/src/repositories/contractLineRepository.ts now defaults missing live-line cadence_owner reads to 'client', persists 'client' when cloning template lines into live contract lines, and backfills 'client' on touched legacy updates instead of leaving contract_lines.cadence_owner unset
    • packages/billing/src/actions/contractLineMappingActions.ts and packages/billing/src/models/contractLineMapping.ts now normalize legacy mapping payloads so template and live contract-line association flows return cadence_owner: 'client' when older rows do not yet carry the column, and touched live association writes preserve/backfill that default
    • server/src/lib/api/services/ContractLineService.ts now normalizes list, listWithOptions, getById, and setPlanActivation responses through one cadence-owner compatibility helper and persists 'client' on touched legacy update paths when callers omit cadence owner
    • packages/clients/src/actions/clientContractLineActions.ts now preserves the existing cadence owner when editing client contract lines and backfills 'client' when legacy rows are edited without an explicit cadence owner, instead of letting writes keep the column null
  • (2026-03-17) billing_cycle_alignment now has an explicit staged-deprecation seam instead of remaining an unconditional required write input, which closes F090:

    • shared/billingClients/billingCycleAlignmentCompatibility.ts now defines one compatibility resolver for legacy alignment values, preserving explicit stored values when present and deriving a readable fallback from enable_proration when new writes omit the field
    • server/src/lib/api/schemas/contractLineSchemas.ts now allows fixed-plan config create payloads to omit billing_cycle_alignment, which stops new API writes from treating the legacy field as mandatory while leaving response payloads readable
    • packages/billing/src/models/contractLineFixedConfig.ts now uses the compatibility resolver on reads and on update/upsert writes, so fixed-config writes can omit the field without losing a readable legacy alignment value
    • server/src/lib/api/services/ContractLineService.ts and both server/package contract-line repositories now route fixed-config and template-clone alignment handling through the same compatibility resolver rather than hard-coding 'start' at each seam
  • (2026-03-17) The first live recurring contract-line configuration surface now presents cadence ownership in business language, which closes F091:

    • packages/billing/src/components/billing-dashboard/contract-lines/FixedContractLineConfiguration.tsx now shows a dedicated Cadence Owner section with Invoice on client billing schedule and Invoice on contract anniversary business labels instead of leaking internal field names into the UI
    • the contract-anniversary option is currently rendered but disabled with rollout copy, which keeps the UI aligned with F088's server-side contract-cadence guard instead of implying that the unsupported path is already available
    • saves now carry the selected cadence-owner value through updateContractLine(...), so the fixed recurring line editor remains the first coherent live configuration surface for cadence-owner semantics before wizard/template flows are updated
  • (2026-03-17) Credit-reader invoice context now stays on canonical recurring detail metadata, which closes F078 without pretending the blocked DB integration is done:

    • packages/billing/src/actions/creditActions.ts now loads source invoices through Invoice.getById(...) for both getCreditDetails(...) and the invoice-summary enrichment inside listClientCredits(...), instead of rereading raw invoices rows that dropped recurring invoice_charge_details
    • the credit list path now exposes invoice_service_period_start / invoice_service_period_end summary fields derived from hydrated recurring invoice charges, so credit-management screens and support tooling keep stable recurring period context even after credit issuance or application
    • server/src/test/unit/billing/creditActions.servicePeriods.test.ts closes T098 with one focused contract covering both the summary list path and the detailed credit reader against a recurring negative-invoice source
    • T100 remains open because the broader DB-backed credit/prepayment integration seam is still blocked locally and is shared with F058/F077
  • (2026-03-17) Recurring billing workflow-event metadata is now explicitly locked on the service-period-first path, which closes F079:

    • server/src/test/unit/billing/recurringBillingRunActions.test.ts now closes T079 by proving the started/completed recurring billing run events keep stable runId, actor metadata, tenant correlation metadata, and completion counts even after recurring invoice generation moved to due-service-period preselection
    • no production code change was needed for this checkpoint; the value was converting an already-correct event contract into an executable guard before broader scheduler-identity work (F151+)
  • (2026-03-17) Duplicate prevention and rerun idempotency now have an explicit action-layer contract, which closes F080:

    • packages/billing/src/actions/invoiceGeneration.ts now throws an explicit duplicate error contract (code = DUPLICATE_RECURRING_INVOICE) when a billing cycle already has an invoice, instead of surfacing the misleading legacy "No active contract lines for this period" message
    • packages/billing/src/actions/recurringBillingRunActions.ts now treats that duplicate error as an idempotent skip during reruns, so automatic recurring runs do not mark already-billed cycles as failures while still refusing to create a second invoice
    • server/src/test/unit/billing/invoiceGeneration.duplicate.test.ts closes T086 by proving the direct generate action blocks the second invoice for the same due window with the explicit duplicate error payload
    • server/src/test/unit/billing/recurringBillingRunActions.test.ts closes T082 by proving reruns skip duplicate cycles without incrementing failedCount
    • server/src/test/unit/billing/billingEngine.timing.test.ts closes T087 by proving an already-persisted advance service period suppresses rebilling even when the enclosing invoice-window metadata is later, which keeps billed-through enforcement anchored to canonical service periods
  • (2026-03-17) Client billing schedule UI and preview flows now frame client cadence as one cadence-owner option instead of the only recurring timing source, which closes F096:

    • packages/billing/src/components/billing-dashboard/BillingCycles.tsx now explains that the dashboard billing-schedule table previews invoice windows for client-cadence recurring lines only, while contract-anniversary lines can follow their own cadence outside this surface
    • packages/clients/src/components/clients/ClientBillingSchedule.tsx now describes the per-client billing schedule as the client-cadence path, adds preview copy clarifying that the dialog shows client-cadence invoice windows, and explicitly states that contract-anniversary lines are previewed separately at the line level
    • packages/billing/tests/billingDashboardRecurringCopy.wiring.test.ts and server/src/test/unit/billing/ClientBillingSchedule.ui.test.tsx now close T117, including a small harness repair so the client-schedule UI test imports the real package component and mocks its actual billingHelpers dependency
  • (2026-03-17) Client portal invoice and contract-line readers now surface recurring timing metadata instead of flattening it away, which closes F097:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.tsx now renders canonical recurring service_period_start / service_period_end ranges and billing_timing badges directly on invoice line items, so client-visible invoice details keep the recurring coverage window instead of only showing pricing fields
    • packages/client-portal/src/actions/client-portal-actions/client-billing.ts now selects billing_timing and cadence_owner for the client contract-line reader, and packages/client-portal/src/components/billing/ContractLineDetailsDialog.tsx now shows cadence-owner and billing-timing labels with a short recurring service-period explanation
    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx and packages/client-portal/src/components/billing/ContractLineDetailsDialog.servicePeriods.test.tsx now close T121 and T122
  • (2026-03-17) Client-portal billing metrics and recent invoice readers now keep canonical recurring period meaning instead of flattening back to invoice-header-only semantics, which closes F130:

    • packages/client-portal/src/actions/client-portal-actions/dashboard.ts now aggregates recent invoice activity through invoice_charge_details, so invoice activity descriptions prefer canonical recurring service_period_start / service_period_end ranges when detail rows exist and fall back cleanly for historical invoices
    • the same dashboard action now formats invoice totals as currency in recent activity descriptions instead of printing raw cents, which keeps the new service-period explanation readable for client users
    • packages/client-portal/src/actions/client-portal-actions/client-billing.ts#getCurrentUsage(...) now uses explicit [period_start, period_end) bucket-period filtering (period_start <= currentDate < period_end) instead of the old inclusive BETWEEN predicate, aligning portal billing-overview usage with the canonical recurring boundary model
    • packages/client-portal/src/actions/client-portal-actions/client-billing-metrics.ts#getClientBucketUsage(...) was already period-aware; packages/client-portal/src/actions/client-portal-actions/client-billing.bucketPeriods.test.ts now locks the remaining-units projection so portal bucket reporting stays tied to the explicit allowance period it represents
    • focused regression coverage now lives in packages/client-portal/src/actions/client-portal-actions/dashboard.recurringServicePeriods.test.ts, packages/client-portal/src/actions/client-portal-actions/client-billing.bucketPeriods.test.ts, plus the existing detail/preview reader tests in packages/client-portal/src/components/billing/*.servicePeriods.test.tsx
  • (2026-03-17) Contract reporting now treats recurring YTD revenue as a service-period-based fact instead of an invoice-header-date fact, which closes F098:

    • packages/billing/src/actions/contractReportActions.ts now aggregates contract revenue YTD from invoice_charge_details.service_period_end when canonical recurring detail rows exist, while falling back to invoices.invoice_date for historical/manual rows that still lack detail periods
    • the fixed/product/license persistence assumptions are now explicit in reporting code: fixed detail-backed rows sum invoice_charge_fixed_details.allocated_amount, while current non-fixed recurring parents still rely on the existing one-detail-per-parent write contract
    • packages/reporting/src/lib/reports/definitions/contracts/revenue.ts now matches that runtime behavior with a raw-SQL metric that uses canonical recurring service-period dates instead of invoice headers when detail rows exist
    • packages/reporting/src/lib/reports/definitions/contracts/expiration.ts and packages/billing/src/components/billing-dashboard/reports/ContractReports.tsx now clarify the reporting date basis in business language so contract-expiration reporting stays assignment-based while revenue reporting is explicitly service-period-based
    • focused regression coverage now lives in server/src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts, packages/reporting/src/lib/reports/definitions/contracts/revenue.servicePeriods.test.ts, and packages/billing/tests/ContractReports.revenueCopy.wiring.test.ts
  • (2026-03-17) Contract reporting summary and adjacent report surfaces now make the clarified date basis visible instead of leaving it implicit, which closes F129:

    • packages/billing/src/components/billing-dashboard/reports/ContractReports.tsx now renders the already-computed atRiskDecisionCount as a fourth summary card (Renewal Decisions Due), so the summary surface exposes renewal-decision timing alongside the recurring YTD service-period metric
    • server/src/test/unit/contractReportActions.summary.servicePeriods.test.ts now proves getContractReportSummary() combines canonical recurring service_period_end YTD totals with decision-due-date risk counts, instead of silently reverting the summary to invoice-header timing
    • server/src/test/unit/contractReportActions.expiration.servicePeriods.test.ts now proves contract expiration rows remain assignment-end and renewal-decision based, independent of recurring invoice service-period projection
    • packages/reporting/src/lib/reports/definitions/contracts/expiration.servicePeriods.test.ts, packages/reporting/src/actions/reconciliationReportActions.servicePeriods.test.ts, and packages/billing/tests/ContractReports.summaryCopy.wiring.test.ts now lock the remaining reporting seams: expiration remains assignment-based, reconciliation remains discrepancy-based, and the dashboard summary copy surfaces the new semantics explicitly
  • (2026-03-17) Accounting export line selection now preserves canonical recurring service periods instead of falling back to invoice header windows, which closes F099:

    • packages/billing/src/services/accountingExportInvoiceSelector.ts now joins invoice_charge_details, collapses multiple detail rows back to one export preview line per charge, and prefers canonical detail-level service_period_start / service_period_end over invoices.billing_period_start / billing_period_end
    • export preview and batch creation still preserve a safe fallback for historical/manual charges that do not have canonical detail rows yet, so export lines remain perioded without forcing detail backfills
    • this makes the downstream adapter contract explicit instead of accidental: QuickBooks Online service dates and Xero payload service-period metadata now continue to consume the selector's canonical line periods rather than invoice headers
    • server/src/test/integration/accounting/invoiceSelection.integration.test.ts now seeds invoice_charge_details for the multi-period export scenario so the intended DB-backed regression is in place, but local execution remains blocked by ECONNREFUSED to Postgres on 127.0.0.1:5438
    • executable local coverage for the same behavior now lives in server/src/test/unit/accounting/accountingExportInvoiceSelector.servicePeriods.test.ts, packages/billing/tests/accountingExportInvoiceSelector.servicePeriods.wiring.test.ts, and packages/billing/tests/accountingExportAdapters.servicePeriods.wiring.test.ts
  • (2026-03-17) Invoice template preview samples now carry canonical recurring service periods end to end, which closes F133:

    • packages/billing/src/utils/sampleInvoiceData.ts now seeds recurring sample charges with service_period_start, service_period_end, billing_timing, and multi-period recurring_detail_periods instead of leaving preview/demo invoices periodless
    • packages/billing/src/utils/sampleInvoicePreview.ts now centralizes sample-to-renderer mapping so preview/demo flows preserve canonical recurring period fields rather than dropping them in InvoiceTemplateManager
    • packages/billing/src/components/billing-dashboard/InvoiceTemplateManager.tsx now initializes its sample preview state with useEffect and the shared mapper, which removes the old render-time state setter and keeps recurring sample metadata intact
    • packages/billing/src/components/invoice-designer/preview/sampleScenarios.ts now includes canonical servicePeriodStart, servicePeriodEnd, billingTiming, and recurringDetailPeriods on preview demo items so designer previews can render the same recurring metadata shape as live invoice previews
    • packages/billing/tests/sampleInvoicePreview.test.ts closes T130 with source-backed coverage for both manager-backed sample invoices and designer preview sample scenarios
  • (2026-03-17) Remaining cadence-owner helper seams are now explicit in test utilities and migration fixtures, which closes F134:

    • server/src/test/test-utils/pricingScheduleHelpers.ts now exports buildRecurringPricingScheduleFixture(...), a shared cadence-owner-aware pricing-schedule fixture builder that composes the canonical monthly recurring fixture and exposes the resulting invoice window, service periods, and pricing-schedule dates in one place
    • server/migrations/utils/client_owned_contracts_simplification.cjs now preserves explicit cadence_owner values and backfills legacy-null clone fixtures to 'client', so migration clone-plan tests no longer depend on incidental object spreads to keep recurring cadence semantics
    • server/src/test/unit/billing/pricingScheduleHelpers.recurringFixtures.test.ts and server/src/test/unit/migrations/clientOwnedContractsSimplificationMigration.test.ts now lock those helper seams with client-cadence and contract-cadence fixture coverage
  • (2026-03-17) Cadence-owner backfill verification now has an explicit non-mutation contract, which closes F135 and T110:

    • server/src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts now asserts the 20260317170000_add_cadence_owner_to_contract_lines.cjs migration never references invoices, invoice_charges, or invoice_charge_details, so the backfill stays scoped to contract_lines
    • the same test still executes the migration against fake legacy contract_lines rows and proves the only mutation is null cadence_owner backfill to 'client', which makes the “safe for untouched tenants” claim explicit instead of only implied by earlier parity checks
  • (2026-03-17) Mixed-cadence rollout validation is now explicit across package actions, API schemas, and UI copy, which closes F136, T143, T144, and T145:

    • shared/billingClients/cadenceOwnerRollout.ts now uses one explicit rollout message that names both contract-owned cadence and mixed-cadence billing as staged, so every layer rejects the same unsupported state with the same wording
    • packages/billing/src/actions/contractLineAction.ts, packages/billing/src/actions/contractLinePresetActions.ts, and packages/billing/src/actions/contractWizardActions.ts now all call assertSupportedCadenceOwnerDuringRollout(...) before persisting live, preset-backed, or wizard-authored contract cadence writes
    • server/src/test/unit/api/contractLineCadenceOwner.schema.test.ts now proves both create and update client-line schema writes reject contract cadence during rollout, while server/src/test/unit/api/contractLineService.cadenceOwner.test.ts continues to guard the API service layer
    • packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts and packages/billing/tests/cadenceOwnerRollout.actions.wiring.test.ts now lock the package action layer, and packages/billing/tests/fixedContractLineConfiguration.cadenceOwner.ui.test.tsx now proves the UI explains the staged mixed-cadence block instead of only disabling the option silently
  • (2026-03-17) Fixed-config model/service compatibility now closes the remaining staged-deprecation seam for billing_cycle_alignment, which closes F137 and T161:

    • packages/billing/src/models/contractLineFixedConfig.ts now routes delete/reset behavior through resolveBillingCycleAlignmentForCompatibility(...) instead of hard-coding a raw alignment value, so every fixed-config write path uses the same compatibility resolver
    • server/src/lib/api/services/ContractLineService.ts now resolves legacy alignment through the compatibility helper when creating fixed-plan configs and when copying fixed config between plans, which removes the last direct ?? 'start' fallback from the service layer
    • packages/billing/tests/billingCycleAlignmentCompatibility.model.test.ts and server/src/test/unit/api/contractLineService.billingCycleAlignmentCompatibility.wiring.test.ts now prove fixed-config writes and copies can omit billing_cycle_alignment while still keeping a readable compatibility value
  • (2026-03-17) Internal recurring-timing docs now describe the live service-period-first model and rollout defaults, which closes F100:

    • shared/billingClients/recurringTiming.ts now carries a module-level architecture reference that spells out the current runtime truth chain: cadence owner -> service periods -> invoice windows -> invoice detail persistence, plus the staged default of client cadence
    • packages/reporting/src/actions/report-actions/README.md now documents the reporting date-basis policy for the current rollout: recurring report actions should prefer canonical service-period detail fields when present, while historical/manual rows may still fall back to invoice dates
    • focused docs contract coverage now lives in packages/billing/tests/recurringTiming.architectureDocs.wiring.test.ts and packages/reporting/src/actions/report-actions/README.servicePeriods.test.ts
  • (2026-03-17) Contract-owned cadence now has executable anniversary-based boundary definitions for monthly, quarterly, semi-annual, and annual recurring lines, which closes F101 through F104:

    • shared/billingClients/contractCadenceServicePeriods.ts now defines calendar-unit contract cadence helpers anchored to anchorDate, which is treated as the assignment-start anniversary rather than a client billing schedule anchor
    • monthly, quarterly, semi-annual, and annual periods all compute each boundary from the original assignment anchor instead of chaining from the prior constrained date, which preserves anniversary intent across short months and longer calendar cycles
    • this checkpoint remains intentionally domain-only: live writes, mixed-cadence selection, first/final invoice rules, and scheduler identity are still deferred to later contract-cadence features (F106+)
    • the generator records timingMetadata.anchorDate plus boundarySource = assignment_start_date, which makes the contract-anchor rule explicit without changing invoice selection behavior yet
    • focused regression coverage now lives in server/src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts, including the acceptance-shape 8th-anchor scenarios for monthly, quarterly, semi-annual, and annual cycles plus the required-anchor guard
  • (2026-03-17) Contract-cadence lifecycle rules are now explicit for future starts and renewals, which closes F105:

    • shared/billingClients/contractCadenceServicePeriods.ts now exports resolveContractCadenceAnchorDate(...), which defines the rollout rule plainly: future-start assignments and renewed contracts anchor to the new assignment start, while renew-in-place preserves the prior anniversary anchor when one exists
    • monthly contract-cadence generation already honored the future-start boundary by not emitting pre-anchor service periods; this checkpoint makes that behavior intentional and names the renewal-policy seam instead of leaving it to caller convention
    • focused regression coverage for those lifecycle rules now lives in server/src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts, which proves both the future-start no-prebilling behavior and the split between renew-in-place versus renewed-contract anchor resolution
  • (2026-03-17) Contract-cadence first/final invoice behavior is now explicit before mixed-cadence scheduler work, which closes F106 and F107:

    • shared/billingClients/contractCadenceServicePeriods.ts now exports resolveContractCadenceInvoiceWindowForServicePeriod(...), which makes the missing policy explicit: contract cadence owns the due invoice window as well as the service-period boundary
    • for advance, the first and final invoices stay on the same anniversary window as the due service period; client billing cycles do not pull those invoices back to the enclosing client cycle
    • for arrears, the first and final invoices stay on the next anniversary window after the covered service period ends, including a partial final period caused by termination mid-period
    • server/src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts now closes T137 and T138 by proving both due-position variants against a monthly 8th-anchor scenario without enabling live contract-cadence execution yet
  • (2026-03-17) Mixed cadence grouping policy is now explicit at the shared-domain layer before scheduler identity changes, which closes F108 and F109:

    • shared/billingClients/recurringTiming.ts now exports groupDueServicePeriodsByInvoiceWindow(...), which groups due work by [start, end) invoice-window identity and carries the set of cadence owners on each group for explainability
    • the rule is now explicit: cadence owner alone does not force a split when client-owned and contract-owned due work lands on the same invoice window, but differing invoice windows always produce separate invoice candidates
    • this checkpoint intentionally stops before F110: live recurring runs still iterate raw billingCycleIds, but later scheduler work can now build on a stable grouping contract instead of re-deciding the product rule
    • server/src/test/unit/billing/recurringTiming.domain.test.ts now closes T141 and T142
  • (2026-03-17) Mixed-cadence live execution remains blocked on the current billing-cycle run identity:

    • packages/billing/src/actions/invoiceGeneration.ts and packages/billing/src/actions/recurringBillingRunActions.ts still model one recurring run input as one billingCycleId / one client invoice window
    • packages/billing/src/lib/billing/billingEngine.ts still builds live recurring timing selections around the current billing window, so mixed cadence that lands on different windows cannot be executed coherently until the later execution-identity work (F110, F151+)
  • (2026-03-17) Mixed-cadence selection is now deterministic on the current billing-cycle run path, which closes F110 without claiming the later scheduler rewrite:

    • packages/billing/src/lib/billing/billingEngine.ts now branches recurring timing selection by cadence_owner, using client-cadence periods for client lines and the shared contract-cadence generator plus contract-owned invoice-window matching for contract lines
    • on the current billingCycleId-driven run path, contract-owned lines are billed only when their contract-owned due window exactly matches the active invoice window; differing contract windows are skipped instead of being pulled into the enclosing client billing cycle
    • recurring timing selections are now built in stable client_contract_line_id order, and server/src/test/unit/billing/billingEngine.timing.test.ts closes T146 by proving the same mixed-cadence result is produced regardless of input order
    • broader execution identity, background job payloads, and truly schedulable contract-cadence windows are still deferred to F151+; this checkpoint only makes the existing run path deterministic when mixed cadence data is present
  • (2026-03-17) The rollout order is now explicit enough to close F111 and T151:

    • PASS0_RECURRING_TIMING_APPENDIX.md now carries a dedicated Staged Rollout Plan section with five concrete stages: additive groundwork, client-cadence parity comparison, client-cadence cutover, contract-cadence enablement, and cleanup/deletion
    • the rollout rule is now source-backed instead of implied: contract cadence stays blocked on live write paths through Stage 2 and Stage 3, and only becomes tenant-writable after client-cadence parity sign-off plus post-cutover stability checks
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now locks that rollout order as T151, so future edits cannot quietly reintroduce contract-cadence enablement ahead of client-cadence parity
    • while landing that docs checkpoint, pass-0-source-inventory.json was refreshed to match current billing_cycle_alignment and persisted service-period reader references so the appendix contract suite is green again instead of hiding behind stale inventory drift
  • (2026-03-17) First-cutover guardrails now explicitly keep out-of-scope billing families out of recurring rollout comparison and preselection, which closes F115:

    • packages/billing/src/lib/billing/billingEngine.ts now skips contract_line_type = Hourly|Usage when precomputing recurringTimingSelections, so the service-period-first preselection seam only targets recurring fixed/product/license-style lines instead of every contract line
    • packages/billing/src/actions/invoiceGeneration.ts now compares only recurring-backed charges in RECURRING_BILLING_COMPARISON_MODE, filtering to canonical recurring charge families with client_contract_line_id before building drift snapshots
    • that means time entries, usage records, and client-scoped material charges cannot produce false rollout drift warnings or appear to be part of the recurring hard cutover just because they share an invoice window with recurring work
    • manual-only invoice creation was already isolated from recurring selection in T084; this checkpoint keeps that existing boundary intact while tightening the live recurring engine and comparison seams
  • (2026-03-17) DB-backed client-cadence parity validation now closes F112, T152, and T153:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now proves both monthly and annual client-cadence recurring invoices keep the same mixed advance/arrears service periods and persisted invoice-window headers under the service-period-first engine
    • the integration fixture now creates a billing location with an email address and reuses one client_contract_id for mixed recurring lines, which keeps the test on the real generateInvoice(...) path without tripping unrelated billing-email or multi-contract invoice guards
    • server/test-utils/billingTestHelpers.ts#createFixedPlanAssignment(...) now accepts optional existing contractId / clientContractId so DB-backed recurring tests can model multiple lines on one client contract instead of fabricating invalid multi-contract invoice fixtures
    • local DB validation is now running successfully against the Docker Postgres listener on 127.0.0.1:57433
  • (2026-03-17) Post-cutover DB-backed sanity coverage now reaches all currently migrated recurring families, which closes F118 plus T171-T174:

    • server/src/test/integration/billingInvoiceTiming.integration.test.ts now adds four focused end-to-end sanity cases on the real generateInvoice(...) path: monthly fixed, quarterly fixed, recurring product, and recurring license invoice generation all persist canonical recurring service periods under client cadence
    • the new product/license fixture helper intentionally uses the same contract-line tables as live recurring execution and marks catalog rows with service_catalog.item_kind = 'product' plus is_license, so the integration tests exercise the migrated quantity-charge paths rather than a test-only shortcut
    • those DB-backed tests surfaced one real runtime seam instead of a harness issue: recurring product/license charges were not carrying config_id, which made invoice_charge_details.config_id inserts fail on real persistence even though the unit tests had already proven the timing math
    • packages/billing/src/lib/billing/billingEngine.ts, packages/billing/src/services/invoiceService.ts, packages/types/src/interfaces/billing.interfaces.ts, and server/src/interfaces/billing.interfaces.ts now thread config_id through recurring product/license charges and into detail persistence, which keeps canonical recurring detail rows writable for all migrated charge families
  • (2026-03-17) Operator/developer rollout guidance now has an executable runbook contract, which closes F119 and T160:

    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RUNBOOK.md now provides concrete parity-check commands, DB-backed sanity validation, comparison-mode guidance, mixed-cadence troubleshooting steps, inspection SQL, and rollback posture
    • server/src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts now enforces that the runbook keeps those exact sections and command examples instead of drifting into non-actionable prose
    • refreshing the runbook contract also forced a pass-0 inventory refresh in pass-0-source-inventory.json, which now matches the live grep-backed source after the recent cleanup and preview-reader checkpoints
  • (2026-03-17) The time-and-usage follow-on boundary is now explicit instead of remaining an unresolved v1 blur, which closes F120:

    • PRD.md now states that time-entry billing and usage-record billing remain event-driven even after recurring materialized service periods land, and that any unification requires a separate follow-on plan
    • PASS0_RECURRING_TIMING_APPENDIX.md now adds a dedicated Follow-On Boundary — Full Time And Usage Unification section that spells out the trigger conditions for such a follow-on and the implementation questions it must answer before those domains can adopt canonical service periods
  • (2026-03-17) Billing-settings defaults now expose recurring cadence rollout metadata without pretending contract cadence is configurable yet, which closes F121:

    • packages/billing/src/actions/billingSettingsActions.ts now returns additive recurring cadence defaults on both tenant and client billing-settings readers: defaultRecurringCadenceOwner = 'client', rollout state client_only, and the shared contract-cadence rollout-block message
    • the same action now rejects any attempted defaultRecurringCadenceOwner = 'contract' write on update paths, so billing-settings surfaces cannot silently bypass the staged contract-cadence guard even though the settings shape now exposes the rollout posture explicitly
    • packages/types/src/interfaces/billing.interfaces.ts, server/src/interfaces/billing.interfaces.ts, and server/src/lib/api/schemas/financialSchemas.ts now carry the same additive default/rollout metadata on billing-settings read models and API schemas, keeping the contract consistent wherever billing defaults are read
  • (2026-03-17) Client billing schedule anchor actions now expose explicit client-cadence scope metadata instead of leaving that meaning as UI-only copy, which closes F122:

    • shared/billingClients/clientCadenceScheduleContext.ts now defines one shared client-cadence schedule context with explicit change-scope, schedule, and preview messaging for client-owned invoice windows
    • shared/billingClients/billingSchedule.ts and packages/billing/src/actions/billingCycleAnchorActions.ts now attach that context to getClientBillingCycleAnchor(...) and schedule-preview results, so action consumers can tell that these windows apply only to client-cadence recurring lines and that contract-anniversary lines are configured separately
    • packages/clients/src/components/clients/ClientBillingSchedule.tsx now renders those messages from the returned action contract instead of hardcoding the cadence-owner explanation locally, which keeps the preview flow aligned with explicit cadence ownership at the data-contract layer as well as the UI layer
  • (2026-03-17) Preset-backed recurring cadence defaults now persist on the preset itself instead of being implicit or alignment-adjacent, which closes F123 and T118:

    • server/migrations/20260317193000_add_cadence_owner_to_contract_line_presets.cjs adds contract_line_presets.cadence_owner with safe backfill to 'client' plus a client|contract check constraint, giving preset-backed recurring defaults their own storage surface instead of piggybacking on billing_cycle_alignment
    • packages/types/src/interfaces/billing.interfaces.ts, server/src/interfaces/billing.interfaces.ts, and packages/billing/src/models/contractLinePreset.ts now expose and normalize preset cadence_owner, defaulting missing legacy rows to 'client' on reads and preserving an existing stored cadence owner on updates when callers omit the field
    • packages/billing/src/actions/contractLinePresetActions.ts now copies preset-backed recurring lines using preset.cadence_owner ?? 'client' when no override is provided, so preset reuse reads an explicit cadence-owner default instead of inferring anything from fixed-config billing_cycle_alignment
  • (2026-03-17) Contract-line mapping/disambiguation helpers now have an executable audit showing they no longer infer recurring timing from legacy fixed-config flags after the cadence-owner cutover, which closes F124 and adds T331:

    • packages/billing/tests/contractLineMappingRecurringTiming.wiring.test.ts now locks the intended boundary explicitly: after the template-snapshot seam, the mapping and disambiguation helpers must operate on explicit cadence_owner fields and may not branch on billing_cycle_alignment or enable_proration
    • this checkpoint did not require a runtime patch because the mapping model/action code was already using explicit cadence-owner defaults; the missing work was the focused audit contract proving that the legacy fixed-config flags no longer participate in live mapping/disambiguation decisions
  • (2026-03-17) Full invoice-detail readers now expose canonical recurring period metadata more consistently instead of flattening everything down to one parent-level summary, which closes F125 and T090:

    • packages/types/src/interfaces/invoice.interfaces.ts and server/src/interfaces/invoice.interfaces.ts now expose additive recurring_detail_periods on IInvoiceCharge, preserving the canonical detail rows behind a recurring parent charge without breaking existing summary fields
    • packages/billing/src/models/invoice.ts now attaches sorted recurring_detail_periods arrays to recurring invoice charges, keeps the existing aggregated service_period_start / service_period_end / billing_timing summary for compatibility, and orders invoice-charge reads chronologically by recurring service period before falling back to original charge order for manual or non-perioded rows
    • that makes both Invoice.getById(...) and the dashboard-facing getInvoiceLineItems(...) path more stable: recurring invoice-detail queries can keep their canonical metadata while still presenting predictable line ordering to dialogs and editors
  • (2026-03-17) Renderer-facing invoice read paths now keep canonical recurring detail periods instead of collapsing them back to one flattened range, which closes F126:

    • packages/types/src/lib/invoice-renderer/types.ts and packages/billing/src/lib/adapters/invoiceAdapters.ts now carry additive recurringDetailPeriods on rendered invoice items, deriving compatibility summary fields from those details only when needed so dashboard preview, client portal preview, PDF generation, and designer preview all keep the canonical detail structure available
    • packages/client-portal/src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx and packages/billing/src/lib/adapters/invoiceAdapters.test.ts now lock that projection contract explicitly, including the case where a recurring parent line has more than one canonical detail period
    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.tsx now renders multiple recurring detail periods as an explicit list when present instead of always flattening back to one service-period badge, which keeps client-facing invoice reads aligned with the canonical detail hydration added in F125
  • (2026-03-17) Accounting export persistence now carries canonical recurring detail periods directly instead of reducing every export line to one legacy summary window, which closes F127:

    • packages/types/src/interfaces/accountingExport.interfaces.ts, packages/billing/src/repositories/accountingExportRepository.ts, and packages/billing/src/actions/accountingExportActions.ts now expose additive recurring_detail_periods metadata on export preview lines and persisted AccountingExportLine.payload, giving repository and controller code one typed place to carry canonical recurring export detail without changing the existing summary service_period_start / service_period_end columns
    • packages/billing/src/services/accountingExportInvoiceSelector.ts now selects invoice_charge_details.billing_timing alongside canonical start/end dates, emits sorted recurringDetailPeriods on preview rows, and persists those exact periods into export-line payloads during batch creation
    • server/src/lib/api/controllers/ApiCSVAccountingController.ts now forwards the same canonical recurring detail payload when CSV export creates batch lines through the shared export service, keeping manual CSV-triggered batches on the same detail-aware contract as the dashboard export flow
    • server/src/test/unit/accounting/accountingExportInvoiceSelector.servicePeriods.test.ts now proves both seams: preview selection exposes canonical recurring detail periods, and createBatchFromFilters(...) persists them into the export-line payload that downstream adapters and rereads consume
  • (2026-03-17) Recurring billing workflow events now state their service-period-first selection basis explicitly instead of leaving it implicit in builder defaults, which closes F128:

    • shared/workflow/streams/domainEventBuilders/recurringBillingRunEventBuilders.ts now defaults recurring billing run payloads to selectionMode = 'due_service_periods' and windowIdentity = 'billing_cycle_window', making the current run contract explicit about how service-period-first billing still selects work during the billing-cycle-identity phase
    • shared/workflow/runtime/schemas/billingEventSchemas.ts now requires those fields on recurring billing run started/completed/failed payloads, so schema consumers and future automation logic cannot treat the selection basis as an unstated convention
    • packages/billing/src/actions/recurringBillingRunActions.ts now passes the same explicit fields at every publish site instead of relying on implicit builder defaults, keeping the emitted workflow events stable if the builders evolve later
    • server/src/test/unit/billing/recurringBillingRunWorkflowEvents.test.ts and server/src/test/unit/billing/recurringBillingRunActions.test.ts now lock both sides: schema validation and action-layer publish inputs
  • (2026-03-17) Comparison-mode rollout control now closes F113 and T154 without changing live invoice persistence:

    • packages/billing/src/actions/invoiceGeneration.ts now treats RECURRING_BILLING_COMPARISON_MODE=legacy-vs-canonical as an additive action-layer gate on calculateBillingForInvoiceWindow(...)
    • when enabled, the action runs the canonical preselected recurring path first, then executes one legacy-style billing calculation without recurringTimingSelections and logs a structured drift warning if the comparison snapshot differs
    • the canonical billing result is always returned and persisted, so comparison mode can be enabled for rollout validation without mutating live invoice outputs
    • server/src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts now proves the guard calls both billing modes in order, emits drift logging, and still returns the canonical result object unchanged
  • (2026-03-17) Cadence-owner migration backfill is now explicitly validated, which closes F114, T155, and T156:

    • server/migrations/20260317170000_add_cadence_owner_to_contract_lines.cjs was already adding cadence_owner with a default of 'client', but the migration contract had only been source-inspected before this checkpoint
    • server/src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts now executes the migration against a fake Knex contract and proves the backfill writes 'client' only to legacy null rows while preserving existing 'client' and 'contract' values
    • together with the DB-backed client-cadence parity validation from F112, this makes the rollout claim concrete: untouched client-cadence tenants keep client defaults without invoice-output drift
  • (2026-03-17) Invoice reread stability now has an explicit regression guard, which closes T080:

    • server/src/test/unit/billing/invoiceModel.servicePeriods.test.ts now proves that Invoice.getById(...), Invoice.getFullInvoiceById(...), and a second Invoice.getById(...) reread all preserve the same aggregated canonical service-period metadata for a multi-detail recurring charge
    • this keeps invoice reload paths from drifting even when one recurring parent charge spans multiple canonical detail periods
  • (2026-03-17) Preview payloads now surface canonical recurring period metadata instead of hiding it behind totals-only preview rows, which closes T083:

    • packages/billing/src/actions/invoiceGeneration.ts now threads service_period_start, service_period_end, and billing_timing from billing charges into preview invoice items and the WasmInvoiceViewModel.items payload
    • packages/types/src/lib/invoice-renderer/types.ts now exposes those preview-item fields as optional renderer inputs, keeping the preview contract additive for existing consumers
    • server/src/test/unit/billing/invoiceGeneration.preview.test.ts now proves preview state carries the canonical recurring period range and timing badge data alongside the existing totals
  • (2026-03-17) Manual invoice creation now has an executable isolation guard, which closes T084:

    • server/src/test/unit/billing/manualInvoiceActions.recurringIsolation.test.ts executes generateManualInvoice(...) with focused mocks and proves the path persists manual items, returns the hydrated invoice, and never instantiates the recurring BillingEngine or calls due-service-period selection
    • this keeps the service-period-first recurring rollout from silently leaking into manual invoice creation while invoice readers and preview payloads grow more detail-aware
  • (2026-03-17) Manual invoice edit/view paths now have a focused canonical-period compatibility contract, which closes T085:

    • server/src/test/unit/billing/manualInvoiceActions.viewing.test.ts executes updateManualInvoice(...), proves the action still deletes/replaces manual items and triggers recalculation, and confirms the returned reread invoice can still carry canonical recurring detail fields on its charge rows
    • this gives the current manual invoice edit/view seam executable coverage without waiting on the blocked DB-backed manual-invoice suite
  • (2026-03-17) Client-portal overview and payment surfaces now carry canonical recurring service-period summaries, which closes F140, T333, and T334:

    • packages/billing/src/actions/invoiceQueries.ts now projects invoice-level service_period_start / service_period_end summaries from invoice_charge_details into client-facing InvoiceViewModel reads, so portal overview/payment surfaces no longer have to infer recurring timing from invoice headers alone
    • packages/client-portal/src/actions/clientPaymentActions.ts now returns the same canonical summary fields during payment verification, which lets the payment-success flow explain what recurring coverage was settled without rereading invoice headers or relying on proration-style copy
    • packages/client-portal/src/components/billing/BillingOverviewTab.tsx, packages/client-portal/src/components/billing/InvoicesTab.tsx, and packages/client-portal/src/components/billing/PaymentSuccessContent.tsx now surface those canonical summaries in the overview, invoice-payment, and payment-success surfaces with explicit service-period-first wording
    • packages/client-portal/src/components/billing/BillingOverviewTab.servicePeriods.test.tsx and packages/client-portal/src/components/billing/PaymentSuccessContent.servicePeriods.test.tsx lock the two new portal UI seams, while the existing invoice detail/preview portal tests remain green on top of the new summary surfaces
  • (2026-03-17) Company-sync, accounting-mapping, and export-validation audit is now executable, which closes F141, T335, and T336:

    • packages/billing/src/services/companySync/companySyncService.ts and packages/billing/src/services/accountingMappingResolver.ts remain intentionally period-agnostic; the audit now locks that by source contract instead of leaving it implicit
    • packages/billing/src/services/accountingExportValidation.ts now makes the boundary explicit in code comments and carries canonical service_period_start / service_period_end metadata through mapping, tax, realm, client-reference, and payment-term validation errors, so export triage keeps the line-level recurring timing context selected earlier in the export pipeline
    • packages/billing/tests/accountingExportValidation.servicePeriods.wiring.test.ts now locks both sides of the audit: company sync + mapping stay free of header-period assumptions, and export validation continues to preserve canonical service-period context without falling back to invoice-header billing dates

Commands / Runbooks

  • (2026-03-18) DB-backed contract-cadence monthly and annual validation:

    • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T276|T277" --hookTimeout 600000 --coverage.enabled false
  • (2026-03-18) Report-output client-cadence parity validation:

    • cd server && npx vitest run src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts --coverage.enabled false
  • (2026-03-18) Persisted service-period parity comparison validation:

    • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodParity.domain.test.ts src/test/unit/billing/materializeClientCadenceServicePeriods.domain.test.ts src/test/unit/billing/materializeContractCadenceServicePeriods.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted service-period due-selection contract validation:

    • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodDueSelection.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted service-period invoice linkage validation:

    • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodInvoiceLinkage.domain.test.ts src/test/unit/migrations/recurringServicePeriodInvoiceLinkageMigration.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted service-period lifecycle validation:

    • cd server && npx vitest run src/test/unit/billing/recurringServicePeriodLifecycle.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-18) Persisted service-period schema validation:

    • cd server && npx vitest run src/test/unit/migrations/recurringServicePeriodsMigration.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not by the recurring-service-period migration
  • (2026-03-18) Persisted service-period record contract validation:

    • cd server && npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-17) Billing-timing default normalization validation:

    • npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.model.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not by the billing-timing normalization changes
  • (2026-03-17) Recurrence storage model validation:

    • npx vitest run src/test/unit/billing/recurrenceStorageModel.contract.test.ts src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.model.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not by the recurrence-storage-model changes
  • (2026-03-17) Template recurring authoring validation:

    • npx vitest run ../packages/billing/tests/templateWizardBucketOverlay.test.ts ../packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Preset recurring authoring validation:

    • npx vitest run ../packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts ../packages/billing/tests/contractLinePresetRecurringAuthoring.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Recurring authoring policy validation:

    • npx vitest run src/test/unit/billing/recurringAuthoringPolicy.domain.test.ts src/test/unit/api/contractLineService.cadenceOwnerCompatibility.wiring.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts ../packages/billing/tests/contractLineCadenceOwnerCompatibility.repository.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the workspace alias config for package billing tests
    • npx vitest run ../packages/billing/tests/fixedContractLineConfiguration.recurringAuthoring.wiring.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still fails only on the pre-existing packages/billing/src/actions/creditActions.ts(1208,13) narrowing error, not on the recurring authoring policy changes
  • (2026-03-17) Reporting date-basis policy validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts src/test/unit/contractReportActions.summary.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/actions/reconciliationReportActions.servicePeriods.test.ts --coverage.enabled false
      • run from packages/reporting/
  • (2026-03-17) Financial-artifact fallback validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run tests/creditManagement.financialArtifactContext.wiring.test.ts --coverage.enabled false
      • run from packages/billing/
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Credit-note source-date-basis validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/infrastructure/billing/invoices/prepaymentInvoice.test.ts -t "T100" --coverage.enabled false
      • run from server/; currently still blocked by migration-heavy harness drift in that infrastructure suite after setup reaches the targeted test
    • DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts -t "T216" --coverage.enabled false
      • run from server/; current local run is still unstable because the older negative-invoice infrastructure suite tears down during migration/test-context setup
    • DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts -t "T216|T100" --coverage.enabled false
      • run from server/; still blocked by 202409071803_initial_schema.cjs migration bootstrap failure (ROLLBACK - Connection terminated unexpectedly)
  • (2026-03-17) Billing dashboard invoice-generation copy validation:

    • npx vitest run ../packages/billing/tests/invoicingGenerateTab.recurringCopy.wiring.test.ts ../packages/billing/tests/billingDashboardRecurringCopy.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Template cadence-owner storage and clone validation:

    • npx vitest run src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/billing/tests/templateCadenceOwnerRoundTrip.actions.test.ts ../packages/billing/tests/draftContractForResumeActions.test.ts ../packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx vitest run src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error (IInvoice | null vs null), not by the template cadence-owner changes
  • (2026-03-17) billing_cycle_alignment runtime cleanup and portal preview validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts -t "T162|T041|T042|T043|T044|T078|T087" --coverage.enabled false
      • run from server/
    • npx vitest run src/test/unit/billing/billingEngine.cleanupSource.test.ts src/test/unit/billing/billingEngine.timing.test.ts src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/billing/billingEngine.licenseTiming.test.ts src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/test/unit/billingEngine.test.ts -t "T021" --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/client-portal/src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/billing/src/lib/adapters/invoiceAdapters.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts src/test/unit/billingEngine.test.ts --coverage.enabled false
      • broad run still exposes pre-existing failures in server/src/test/unit/billingEngine.test.ts unrelated to this checkpoint (this.knex.select harness drift and missing mocked client lookups in older pricing-schedule tests)
  • (2026-03-16) Recon:

    • rg -n "resolveServicePeriod|applyProrationToPlan|_calculateProrationFactor|billing_cycle_alignment|billing_timing|client_billing_cycles" packages/billing/src/lib/billing server/src/test shared
    • sed -n '220,470p' packages/billing/src/lib/billing/billingEngine.ts
    • sed -n '2284,2595p' packages/billing/src/lib/billing/billingEngine.ts
    • sed -n '60,210p' server/src/test/integration/billingInvoiceTiming.integration.test.ts
    • sed -n '1,260p' shared/billingClients/createBillingCycles.ts
  • (2026-03-17) Replan breadth inventory:

    • rg -n "billing_timing|service_period_start|service_period_end|billing_cycle_alignment|client_billing_cycles|generateInvoice|calculateBilling|billing_period_start|billing_period_end|proration" packages server shared
    • find server/src/test -maxdepth 3 -type f | rg "billing|invoice|contract|renewal|pricing|credit|report"
    • find packages -maxdepth 5 -type f | rg "billing|invoice|contract|renewal|report|pricing|accounting|BillingCycles|ContractLine|contractWizard|manualInvoice|quickBooks|xero"
  • (2026-03-17) Second-pass critique prompts:

    • runtime/dependent billing seams
    • invoice/downstream consumer seams
    • data model/API/UI/migration seams
  • (2026-03-17) Plan validation:

    • python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/validate_plan.py ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership
  • (2026-03-17) Pass-0 appendix validation:

    • npm --prefix server test -- src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts
  • (2026-03-17) Shared recurring-timing validation:

    • npm --prefix server test -- src/test/unit/billing/recurringTiming.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
  • (2026-03-17) Client cadence validation:

    • npx vitest run src/test/unit/billing/clientCadenceServicePeriods.domain.test.ts src/test/unit/billing/recurringTiming.domain.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Shared due-period/coverage validation:

    • npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
  • (2026-03-17) Fixed timing seam validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
    • npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts --coverage.enabled false
      • blocked locally by ECONNREFUSED to Postgres on 127.0.0.1:5438
  • (2026-03-17) Fixed settlement follow-up validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
    • npx vitest run src/test/unit/billingEngine.test.ts --coverage.enabled false
      • broad legacy unit file currently fails outside this seam because its harness no longer satisfies unrelated BillingEngine query expectations (this.knex.select in calculateMaterialCharges, missing client lookup mocks in older pricing-schedule tests)
  • (2026-03-17) Fixed tax regression validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
  • (2026-03-17) Fixed detail persistence and metadata validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/invoiceService.fixedPersistence.test.ts --coverage.enabled false
  • (2026-03-17) Fixed metadata persistence validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/invoiceService.fixedPersistence.test.ts --coverage.enabled false
    • npx vitest run src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts --coverage.enabled false
      • blocked locally by EPERM to Postgres on 127.0.0.1:5438 and 127.0.0.1:5432
  • (2026-03-17) Bucket period mapping validation:

    • npx vitest run src/test/unit/billingEngine.test.ts -t "calculate bucket overlay charges correctly|T056" --coverage.enabled false
  • (2026-03-17) Bucket rollover validation:

    • npx vitest run src/test/unit/billing/billingEngine.bucketTiming.test.ts src/test/unit/billing/bucketUsageService.periods.test.ts --coverage.enabled false
    • npm test -- bucketUsageService.periods.test.ts
  • (2026-03-17) Bucket invoice-window validation:

    • npx vitest run src/test/unit/billing/billingEngine.bucketTiming.test.ts --coverage.enabled false
  • (2026-03-17) Fixed discount and pricing-schedule parity validation:

    • npx vitest run src/test/unit/billing/billingEngine.discountPricingTiming.test.ts --coverage.enabled false
  • (2026-03-17) Discount applicability and PO-association validation:

    • npx vitest run src/test/unit/billing/billingEngine.discountPricingTiming.test.ts src/test/unit/billing/invoiceService.fixedPersistence.test.ts --coverage.enabled false
  • (2026-03-17) Fixed tax-date and negative-recurring validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
  • (2026-03-17) Product-path inventory validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Product recurring timing validation:

    • npx vitest run src/test/unit/billing/billingEngine.productTiming.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/billing/billingEngine.timing.test.ts src/test/unit/billing/billingEngine.discountPricingTiming.test.ts --coverage.enabled false
  • (2026-03-17) License-path inventory validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) License recurring timing validation:

    • npx vitest run src/test/unit/billing/billingEngine.licenseTiming.test.ts src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
  • (2026-03-17) Product/license detail persistence validation:

    • npx vitest run src/test/unit/billing/invoiceService.fixedPersistence.test.ts src/test/unit/billing/billingEngine.licenseTiming.test.ts src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Explicit out-of-scope boundary validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Blocked prepayment follow-up:

    • npx vitest run src/test/infrastructure/billing/invoices/prepaymentInvoice.test.ts -t "T096" --coverage.enabled false
      • blocked locally by ECONNREFUSED to Postgres on 127.0.0.1:5432
  • (2026-03-17) Prepayment recurring-detail reread validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/billing/invoiceService.fixedPersistence.test.ts --coverage.enabled false
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
  • (2026-03-17) Manual/recurring reread validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx vitest run src/test/infrastructure/billing/invoices/manualInvoice.test.ts -t "creates a manual invoice with single line item|correctly updates an invoice when new manual items are added" --coverage.enabled false
      • blocked locally by an existing Vitest module mock error: @alga-psa/auth is mocked without withAuth, so the suite fails during module evaluation before the targeted tests run
  • (2026-03-17) Purchase-order UI safeguard validation:

    • npx vitest run src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx src/test/unit/billing/contractPurchaseOrderSupport.poBanner.ui.test.tsx --coverage.enabled false
  • (2026-03-17) Purchase-order credit-consumption validation:

    • npx vitest run tests/purchaseOrderService.creditConsumption.test.ts --coverage.enabled false
      • run from packages/billing/
    • npx vitest run src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx src/test/unit/billing/contractPurchaseOrderSupport.poBanner.ui.test.tsx --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Tax-source and external-tax service-period validation:

    • npx vitest run tests/invoiceService.externalTax.servicePeriods.wiring.test.ts tests/taxSourceActions.servicePeriods.wiring.test.ts --coverage.enabled false
      • run from packages/billing/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Invoice-generation recurring preselection validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/billing/billingEngine.licenseTiming.test.ts --coverage.enabled false
  • (2026-03-17) Invoice header/finalization validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.headerPeriods.test.ts --coverage.enabled false
  • (2026-03-17) Recurring-run delegation validation:

    • npx vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled false
  • (2026-03-17) Preview recurring-timing validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts --coverage.enabled false
  • (2026-03-17) Empty recurring-selection validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.emptyResult.test.ts --coverage.enabled false
  • (2026-03-17) Credit-flow and billed-through regression validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
  • (2026-03-17) Credit reconciliation / expiration validation:

    • npx vitest run src/test/unit/billing/creditReconciliation.servicePeriods.test.ts --coverage.enabled false
  • (2026-03-17) Contract wizard cadence-owner validation:

    • npx vitest run ../packages/billing/tests/contractWizardResume.test.tsx ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Template wizard cadence-owner validation:

    • npx vitest run ../packages/billing/tests/templateWizardBucketOverlay.test.ts ../packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Fixed recurring copy validation:

    • npx vitest run ../packages/billing/tests/fixedRecurringPartialPeriodCopy.wiring.test.ts ../packages/billing/tests/fixedContractLineConfiguration.cadenceOwner.ui.test.tsx --coverage.enabled false
    • npx vitest run ../packages/billing/tests/billingDashboardRecurringCopy.wiring.test.ts ../packages/billing/tests/fixedRecurringPartialPeriodCopy.wiring.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Cadence-owner persistence validation:

    • npx vitest run src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx vitest run src/cadence-owner-contract-lines.typecheck.test.ts src/interfaces/barrel.test.ts --coverage.enabled false
      • run from packages/types
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Cadence-owner API validation:

    • npx vitest run src/test/unit/api/contractLineCadenceOwner.schema.test.ts src/test/unit/api/contractLineService.cadenceOwner.test.ts src/test/unit/api/contractCreateOwnerClientSchema.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • blocked by an unrelated existing type error in packages/billing/src/actions/creditActions.ts (IInvoice | null narrowing), not by the cadence-owner API changes
  • (2026-03-17) Cadence-owner package authoring validation:

    • npx vitest run ../packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts ../packages/billing/tests/draftContractForResumeActions.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Recurring fixture-builder validation:

    • npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts src/test/unit/billing/billingTestHelpers.recurringFixtures.test.ts --coverage.enabled false
  • (2026-03-17) Cadence-owner rollout validation:

    • npx vitest run src/test/unit/api/contractLineCadenceOwner.schema.test.ts src/test/unit/api/contractLineService.cadenceOwner.test.ts src/test/unit/api/contractCreateOwnerClientSchema.test.ts --coverage.enabled false
  • (2026-03-17) Cadence-owner compatibility validation:

    • npx vitest run src/test/unit/api/contractLineService.cadenceOwner.test.ts src/test/unit/api/contractLineService.cadenceOwnerCompatibility.wiring.test.ts --coverage.enabled false
    • npx vitest run ../packages/billing/tests/contractLineCadenceOwnerCompatibility.repository.test.ts ../packages/billing/tests/contractLineCadenceOwnerCompatibility.wiring.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json && npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Billing-cycle-alignment staged-deprecation validation:

    • npx vitest run src/test/unit/api/billingCycleAlignmentCompatibility.schema.test.ts src/test/unit/api/billingCycleAlignmentCompatibility.repository.wiring.test.ts src/test/unit/api/contractLineService.billingCycleAlignmentCompatibility.wiring.test.ts --coverage.enabled false
    • npx vitest run ../packages/billing/tests/billingCycleAlignmentCompatibility.model.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked by an unrelated pre-existing type error in packages/billing/src/actions/creditActions.ts:979 (IInvoice | null inferred against null)
  • (2026-03-17) Fixed recurring cadence-owner UI validation:

    • npx vitest run ../packages/billing/tests/fixedContractLineConfiguration.cadenceOwner.ui.test.tsx --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Credit-reader canonical invoice-context validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/creditReconciliation.servicePeriods.test.ts src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/billing/creditActions.servicePeriods.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Recurring workflow-event metadata validation:

    • npx vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/recurringBillingRunActions.test.ts src/test/unit/billing/invoiceGeneration.headerPeriods.test.ts --coverage.enabled false
  • (2026-03-17) Duplicate-prevention and billed-through validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.duplicate.test.ts src/test/unit/billing/recurringBillingRunActions.test.ts src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Post-cutover recurring-family sanity validation:

    • DB_PORT=57433 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T171|T172|T173|T174" --coverage.enabled false
    • DB_PORT=57433 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T173|T174" --coverage.enabled false
      • first pass exposed a real persistence seam: product/license detail inserts were failing because runtime charges carried canonical service periods but not config_id
    • npx vitest run src/test/unit/billing/billingEngine.productTiming.test.ts src/test/unit/billing/billingEngine.licenseTiming.test.ts src/test/unit/billing/invoiceService.fixedPersistence.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json && npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Runbook and docs-inventory validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Time/usage follow-on boundary validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) Billing-settings cadence-default validation:

    • npx vitest run ../packages/billing/src/actions/billingSettingsActions.cadenceOwnerDefaultsWiring.test.ts ../packages/billing/src/actions/billingSettingsActions.renewalDefaultsWiring.test.ts --coverage.enabled false
      • run from server/ so the workspace Vitest config can execute the source-level billing action wiring tests
    • npx vitest run src/test/unit/api/defaultBillingSettings.cadenceOwner.schema.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked by the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error, not by the billing-settings cadence-default changes
  • (2026-03-17) Billing-cycle anchor cadence-context validation:

    • npx vitest run src/test/unit/billing/previewClientBillingPeriods.test.ts src/test/unit/billing/ClientBillingSchedule.ui.test.tsx --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Preset cadence-owner persistence validation:

    • npx vitest run ../packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts ../packages/billing/tests/contractLinePresetCadenceOwner.model.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-17) Contract-line mapping recurring-timing audit validation:

    • npx vitest run ../packages/billing/tests/contractLineMappingRecurringTiming.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
  • (2026-03-17) Invoice-detail recurring-period reader validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
  • (2026-03-17) Renderer-facing canonical recurring-period validation:

    • npx vitest run ../packages/billing/src/lib/adapters/invoiceAdapters.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx vitest run src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx --coverage.enabled false
      • run from packages/client-portal/
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Accounting export canonical-detail payload validation:

    • npx vitest run src/test/unit/accounting/accountingExportInvoiceSelector.servicePeriods.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked by the unrelated pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error (IInvoice | null vs null), not by the accounting-export payload changes
  • (2026-03-17) Recurring billing workflow-event schema validation:

    • npx vitest run src/test/unit/billing/recurringBillingRunActions.test.ts src/test/unit/billing/recurringBillingRunWorkflowEvents.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the unrelated pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error
  • (2026-03-17) Client billing schedule cadence-owner copy validation:

    • npx vitest run src/test/unit/billing/ClientBillingSchedule.ui.test.tsx ../packages/billing/tests/billingDashboardRecurringCopy.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest picks up the existing workspace alias config for both server and package tests
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Client portal recurring timing display validation:

    • npx vitest run ../packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx ../packages/client-portal/src/components/billing/ContractLineDetailsDialog.servicePeriods.test.tsx --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for client-portal package tests
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Client-portal billing metrics and recent invoice validation:

    • npx vitest run src/actions/client-portal-actions/dashboard.recurringServicePeriods.test.ts src/actions/client-portal-actions/client-billing.bucketPeriods.test.ts src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx src/components/billing/ContractLineDetailsDialog.servicePeriods.test.tsx src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx --coverage.enabled false
      • run from packages/client-portal/
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
  • (2026-03-17) Contract reporting service-period basis validation:

    • npx vitest run src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts ../packages/billing/tests/ContractReports.revenueCopy.wiring.test.ts ../packages/billing/tests/contractReportActions.summary.wiring.test.ts ../packages/billing/tests/contractReportActions.revenue.assignmentFact.test.ts ../packages/billing/tests/contractReportActions.expiration.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest picks up the existing workspace alias config for package billing tests
    • npx vitest run src/lib/reports/definitions/contracts/revenue.servicePeriods.test.ts --coverage.enabled false
      • run from packages/reporting/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/reporting/tsconfig.json
  • (2026-03-17) Contract reporting summary and expiration validation:

    • npx vitest run src/test/unit/contractReportActions.recurringServicePeriodBasis.test.ts src/test/unit/contractReportActions.expiration.servicePeriods.test.ts src/test/unit/contractReportActions.summary.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run tests/ContractReports.revenueCopy.wiring.test.ts tests/ContractReports.expirationCopy.wiring.test.ts tests/ContractReports.summaryCopy.wiring.test.ts tests/contractReportActions.summary.wiring.test.ts tests/contractReportActions.expiration.wiring.test.ts
      • run from packages/billing/
    • npx vitest run src/lib/reports/definitions/contracts/revenue.servicePeriods.test.ts src/lib/reports/definitions/contracts/expiration.servicePeriods.test.ts src/actions/reconciliationReportActions.servicePeriods.test.ts
      • run from packages/reporting/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Accounting export service-period validation:

    • npx vitest run src/test/unit/accounting/accountingExportInvoiceSelector.servicePeriods.test.ts ../packages/billing/tests/accountingExportInvoiceSelector.servicePeriods.wiring.test.ts ../packages/billing/tests/accountingExportAdapters.servicePeriods.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest picks up the existing workspace alias config for package billing tests
    • npx vitest run src/test/integration/accounting/invoiceSelection.integration.test.ts --coverage.enabled false
      • blocked locally by ECONNREFUSED to Postgres on 127.0.0.1:5438
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Internal recurring-timing docs validation:

    • npx vitest run ../packages/billing/tests/recurringTiming.architectureDocs.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest picks up the existing workspace alias config for package billing tests
    • npx vitest run src/actions/report-actions/README.servicePeriods.test.ts --coverage.enabled false
      • run from packages/reporting/
  • (2026-03-17) Contract-cadence boundary validation:

    • npx vitest run src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts ../packages/billing/tests/recurringTiming.architectureDocs.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest picks up the existing workspace alias config for package billing tests
    • npx vitest run src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts --coverage.enabled false
      • run from server/ for the widened quarterly / semi-annual / annual cadence assertions plus future-start / renewal anchor rules
    • npx vitest run src/actions/report-actions/README.servicePeriods.test.ts --coverage.enabled false
      • run from packages/reporting/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Contract-cadence first/final invoice validation:

    • npx vitest run src/test/unit/billing/contractCadenceServicePeriods.domain.test.ts --coverage.enabled false
  • (2026-03-17) Mixed-cadence grouping validation:

    • npx vitest run src/test/unit/billing/recurringTiming.domain.test.ts --coverage.enabled false
  • (2026-03-17) Invoice reread stability validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
  • (2026-03-17) Preview service-period validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types
  • (2026-03-17) Manual-invoice isolation validation:

    • npx vitest run src/test/unit/billing/manualInvoiceActions.recurringIsolation.test.ts --coverage.enabled false
  • (2026-03-17) Manual-invoice edit/view validation:

    • npx vitest run src/test/unit/billing/manualInvoiceActions.viewing.test.ts --coverage.enabled false
  • (2026-03-17) Mixed-cadence deterministic selection validation:

    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Staged-rollout plan validation:

    • npx vitest run src/test/unit/docs/servicePeriodFirstBillingPlan.contract.test.ts --coverage.enabled false
  • (2026-03-17) First-cutover guardrail validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts --coverage.enabled false
    • npx vitest run src/test/unit/billing/billingEngine.timing.test.ts --coverage.enabled false
  • (2026-03-17) DB-backed client-cadence parity validation:

    • DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/billingInvoiceTiming.integration.test.ts --coverage.enabled false
      • run from server/
  • (2026-03-17) Comparison-mode rollout validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts --coverage.enabled false
      • run from server/
  • (2026-03-17) Cadence-owner backfill validation:

    • npx vitest run src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts --coverage.enabled false
      • run from server/
  • (2026-03-17) Invoice template preview sample validation:

    • npx vitest run tests/sampleInvoicePreview.test.ts --coverage.enabled false
      • run from packages/billing/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Cadence-owner helper fixture validation:

    • npx vitest run src/test/unit/billing/pricingScheduleHelpers.recurringFixtures.test.ts src/test/unit/migrations/clientOwnedContractsSimplificationMigration.test.ts --coverage.enabled false
      • run from server/
  • (2026-03-17) Cadence-owner backfill non-mutation validation:

    • npx vitest run src/test/unit/billing/contractLineCadenceOwner.persistence.test.ts --coverage.enabled false
      • run from server/
  • (2026-03-17) Mixed-cadence rollout validation:

    • npx vitest run src/test/unit/api/contractLineCadenceOwner.schema.test.ts src/test/unit/api/contractLineService.cadenceOwner.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/billing/tests/contractLinePresetCadenceOwner.actions.test.ts ../packages/billing/tests/cadenceOwnerRollout.actions.wiring.test.ts ../packages/billing/tests/fixedContractLineConfiguration.cadenceOwner.ui.test.tsx --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Fixed-config alignment compatibility validation:

    • npx vitest run src/test/unit/api/contractLineService.billingCycleAlignmentCompatibility.wiring.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run ../packages/billing/tests/billingCycleAlignmentCompatibility.model.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Client-portal overview/payment service-period summary validation:

    • npx vitest run ../packages/client-portal/src/components/billing/BillingOverviewTab.servicePeriods.test.tsx ../packages/client-portal/src/components/billing/PaymentSuccessContent.servicePeriods.test.tsx ../packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx ../packages/client-portal/src/components/billing/ClientInvoicePreview.servicePeriods.test.tsx --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package client-portal tests
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • currently still fails on the pre-existing packages/billing/src/actions/creditActions.ts(979,13) type mismatch unrelated to F140
  • (2026-03-17) Accounting service-period audit validation:

    • npx vitest run ../packages/billing/tests/accountingExportValidation.servicePeriods.wiring.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Recurring parent/detail projection validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts src/test/unit/api/invoiceService.recurringDetailProjection.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still blocked only by the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error, not by the recurring projection changes
  • (2026-03-17) Historical flat invoice fallback validation:

    • npx vitest run src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types --coverage.enabled false
  • (2026-03-17) Preview multi-period projection validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts --coverage.enabled false
      • run from server/
    • npx vitest run src/interfaces/barrel.test.ts --root packages/types --coverage.enabled false
    • npx tsc --pretty false --noEmit -p packages/types/tsconfig.json
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Rendering-adapter multi-period projection validation:

    • npx vitest run ../packages/billing/src/lib/adapters/invoiceAdapters.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package source tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Client-portal projection policy validation:

    • npx vitest run ../packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package client-portal tests
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
  • (2026-03-17) Multi-period recurring ordering/aggregation validation:

    • npx vitest run ../packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx ../packages/client-portal/src/components/billing/recurringServicePeriodSummary.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package client-portal tests
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
  • (2026-03-17) Invoice schema compatibility validation:

    • npx vitest run src/test/unit/api/invoiceResponseSchema.compatibility.test.ts src/test/unit/api/invoiceService.recurringDetailProjection.test.ts src/test/unit/billing/invoiceModel.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still fails only on the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error (IInvoice | null vs null), not on the F169 schema changes
  • (2026-03-17) Invoice workflow recurring-provenance validation:

    • npx vitest run src/test/unit/invoiceWorkflowEvents.test.ts src/test/unit/api/invoiceWorkflowRecurringProvenance.wiring.test.ts src/test/unit/api/invoiceResponseSchema.compatibility.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still fails only on the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error (IInvoice | null vs null), not on the F170 workflow/audit provenance changes
  • (2026-03-17) Billed-through detail-reader validation:

    • npx vitest run src/test/unit/billing/billingEngine.billedThroughReader.test.ts src/test/unit/billing/billingEngine.timing.test.ts -t "T201|T087" --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p server/tsconfig.json
      • still fails only on the pre-existing packages/billing/src/actions/creditActions.ts:979 narrowing error (IInvoice | null vs null), not on the F171 billed-through reader contract
  • (2026-03-17) Client contract-line mutation-guard validation:

    • npx vitest run src/test/unit/billing/clientContractLineMutationGuards.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Client contract-line renewal/replacement identity validation:

    • npx vitest run src/test/unit/billing/clientContractLineReplacementIdentity.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Client-contract termination/detail-period validation:

    • npx vitest run src/test/unit/clientContractActions.overlapExclusive.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/clients/tsconfig.json
  • (2026-03-17) Invoice deletion/detail-period safeguard validation:

    • npx vitest run ../packages/billing/tests/invoiceModification.recurringDeletionGuard.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx vitest run src/test/unit/api/invoiceService.deleteRecurringDetailGuard.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Invoice recalculation/detail-period preservation validation:

    • npx vitest run src/test/unit/billing/billingEngine.recalculateInvoice.detailPeriods.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Manual recurring-charge edit guard validation:

    • npx vitest run ../packages/billing/tests/invoiceModification.manualRecurringGuard.test.ts --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing tests
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Persisted-invoice preview refresh validation:

    • npx vitest run ../packages/billing/src/components/invoice-designer/DesignerVisualWorkspace.test.tsx -t "T208" --coverage.enabled false
      • run from server/ so Vitest uses the existing workspace alias config for package billing component tests
    • npx vitest run ../packages/billing/tests/invoiceQueries.recurringDetailRefresh.wiring.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Percentage-discount recalculation validation:

    • npx vitest run src/test/unit/billing/invoiceService.percentageDiscountRecalculation.test.ts src/test/unit/billing/billingEngine.recalculateInvoice.detailPeriods.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Zero-dollar recurring finalization validation:

    • npx vitest run src/test/unit/billing/invoiceGeneration.emptyResult.test.ts src/test/unit/billing/invoiceGeneration.zeroDollarFinalization.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Manual invoice period-policy validation:

    • npx vitest run src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts src/test/unit/billing/invoiceService.percentageDiscountRecalculation.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Manual-to-recurring provenance validation:

    • npx vitest run src/test/unit/billing/invoiceService.manualPeriodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Prepayment financial-artifact validation:

    • npx vitest run src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Negative-invoice offset validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Credit application recurring-context validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Credit reconciliation date-basis validation:

    • npx vitest run src/test/unit/billing/creditReconciliation.servicePeriods.test.ts src/test/unit/billing/creditActions.servicePeriods.test.ts src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Credit transfer lineage validation:

    • npx vitest run src/test/unit/billing/creditActions.servicePeriods.test.ts src/test/unit/billing/creditReconciliation.servicePeriods.test.ts src/test/unit/billing/prepaymentInvoice.periodPolicy.test.ts --coverage.enabled false
      • run from server/
    • npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Financial-artifact display policy validation:

    • npx vitest run src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx src/components/billing/BillingOverviewTab.servicePeriods.test.tsx --coverage.enabled false
      • run from packages/client-portal/
    • npx vitest run tests/creditManagement.financialArtifactContext.wiring.test.ts --coverage.enabled false
      • run from packages/billing/
    • npx tsc --pretty false --noEmit -p packages/client-portal/tsconfig.json
  • (2026-03-17) Client contract assignment-date validation now distinguishes canonical recurring coverage from historical invoice-window fallback, which closes F174 and T204:

    • packages/clients/src/actions/clientContractActions.ts now queries canonical invoice_charge_details coverage for the current client_contract_id before using legacy client_billing_cycles overlap logic, so end-date shortening and mid-cycle termination are enforced against billed recurring service periods rather than against the entire enclosing invoice window whenever authoritative detail rows exist
    • the new rule is explicit and end-exclusive: the earliest billed recurring service_period_start still blocks moving the contract start later into already billed coverage, while the latest billed service_period_end is treated as exclusive and converted into the last already-billed service day when validating a shorter end_date
    • historical flat invoices still keep the old fallback behavior because updateClientContract(...) only consults client_billing_cycles when no canonical recurring detail periods exist for the contract
    • server/src/test/unit/clientContractActions.overlapExclusive.test.ts now proves all three boundary cases together: touching historical invoice-window boundaries still pass under [start, end) semantics, canonical partial service periods allow termination exactly on the last billed day, and shortening earlier than that day is rejected with a detail-period-specific error
  • (2026-03-17) Invoice deletion now preserves authoritative recurring detail periods instead of treating detail-backed invoices like disposable draft rows, which closes F175 and T205:

    • packages/billing/src/actions/invoiceModification.ts now blocks hardDeleteInvoice(...) as soon as canonical invoice_charge_details rows exist for the invoice, with an explicit error instructing callers to cancel instead of deleting
    • server/src/lib/api/services/InvoiceService.ts now computes recurring provenance before delete-path branching and treats canonical-detail-backed invoices as soft-cancel candidates even when they have no payments yet, so the API delete path preserves the invoice plus its authoritative recurring detail history
    • the safeguard intentionally distinguishes detail-backed recurring invoices from historical/manual invoices: invoices without canonical recurring detail periods still follow the existing delete behavior, while draft or finalized recurring invoices with canonical periods are preserved through cancellation semantics
    • focused executable coverage now lives in packages/billing/tests/invoiceModification.recurringDeletionGuard.test.ts for the explicit hard-delete action and server/src/test/unit/api/invoiceService.deleteRecurringDetailGuard.test.ts for the API soft-cancel branch
  • (2026-03-17) Invoice recalculation is now explicitly defined as a financial-only pass that preserves canonical recurring detail periods, which closes F176 and T206:

    • packages/billing/src/lib/billing/billingEngine.ts now documents the intended contract inline at the recalculation seam: once an invoice exists, invoice_charge_details remains the authoritative recurring timing record and recalculation should only recompute tax distribution and invoice totals
    • server/src/test/unit/billing/billingEngine.recalculateInvoice.detailPeriods.test.ts proves that contract directly by exercising BillingEngine.recalculateInvoice(...) with a fake invoice/client store, asserting it delegates only to calculateAndDistributeTax(...) and updateInvoiceTotalsAndRecordTransaction(...), and failing if the recalculation path starts querying invoice_charge_details
  • (2026-03-17) Manual invoice edit flows now make recurring provenance policy explicit instead of silently ignoring edits against recurring parent charges, which closes F177 and T207:

    • packages/billing/src/actions/invoiceModification.ts now checks targeted update/remove item IDs before manual edits proceed and rejects any non-manual invoice charge targets up front
    • the recurring-specific rule is stricter and explicit: if a targeted parent charge already has canonical invoice_charge_details rows, the action throws Cannot manually edit recurring invoice charges once canonical detail periods exist... and tells the caller to add a manual adjustment or cancel/regenerate instead of mutating the recurring line in place
    • packages/billing/tests/invoiceModification.manualRecurringGuard.test.ts proves that recurring-detail-backed invoice charges trip that guard and that the action does not fall through into invoice recalculation after the rejected edit attempt
  • (2026-03-17) Post-persist rerender and preview-refresh flows are now explicitly split between summary-range readers and full detail-aware readers, which closes F178 and T208:

    • packages/billing/src/actions/invoiceQueries.ts now documents the intended split directly in code: paginated/list summary surfaces flatten canonical recurring detail rows to one summary range, while getInvoiceForRendering(...) must keep using Invoice.getFullInvoiceById(...) so persisted recurring detail periods survive rerender and preview refresh
    • packages/billing/src/components/invoice-designer/DesignerVisualWorkspace.test.tsx now proves the persisted existing-invoice preview path forwards recurring_detail_periods into mapDbInvoiceToWasmViewModel(...) instead of stripping them before preview rendering
    • packages/billing/tests/invoiceQueries.recurringDetailRefresh.wiring.test.ts now locks the reader split at the action layer so future refactors cannot collapse rerender flows back onto the summary-only invoice list projection
  • (2026-03-17) Template-to-contract cloning now propagates explicit recurring timing semantics instead of dropping them at the client wizard seam, which closes F207 and T236:

    • packages/billing/src/actions/contractWizardActions.ts now treats billing_timing as part of the client-wizard recurrence contract everywhere the template clone path crosses a boundary: ClientTemplateSnapshot, ClientContractWizardSubmission, and DraftContractWizardData all carry it explicitly; getContractTemplateSnapshotForClientWizard(...) and getDraftContractForResume(...) now reread it from detailed template/live lines; and createClientContractFromWizard(...) now passes submission.billing_timing into resolveRecurringAuthoringPolicy(...) before creating fixed, product, hourly, and usage lines
    • packages/billing/src/components/billing-dashboard/contracts/ContractWizard.tsx now preserves that timing field through wizard state instead of flattening every template-derived clone back to the arrears default: default wizard state carries billing_timing: 'arrears', template snapshot application copies snapshot.billing_timing, and save/create submission payloads emit wizardData.billing_timing ?? 'arrears'
    • packages/billing/tests/templateCadenceOwnerRoundTrip.actions.test.ts now proves template snapshots and resumed drafts preserve both cadence_owner and billing_timing even when the authored recurring line is hourly/usage instead of fixed, which keeps the clone path from relying on incidental fixed-line-only behavior
    • packages/billing/tests/contractWizardResume.test.tsx now proves the live client wizard clone seam carries template-authored billing_timing: 'advance' and cadence_owner: 'contract' into the eventual createClientContractFromWizard(...) save-draft payload after template selection, and the resumed-draft fixture coverage now keeps billing_timing stable across resave
    • packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts now locks the additive source contract so template snapshots and wizard submissions cannot silently stop threading billing_timing alongside cadence_owner
    • validation for this checkpoint used cd server && npx vitest run ../packages/billing/tests/templateCadenceOwnerRoundTrip.actions.test.ts ../packages/billing/tests/contractWizardResume.test.tsx ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts --coverage.enabled false plus npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Template-derived live contract lines now have explicit post-clone independence semantics, which closes F208 and T237:

    • packages/billing/src/repositories/contractLineRepository.ts and server/src/lib/repositories/contractLineRepository.ts now state the intended clone rule inline at cloneTemplateLineToContract(...): cadence owner and billing timing are copied onto the new live contract_lines row at clone time, and later template edits are provenance only rather than retroactive live-line mutations
    • server/src/lib/api/services/ContractLineService.ts now states the same service-layer rule where template lines are assigned to client contracts: template_contract_line_id remains lineage/provenance, but recurring cadence semantics on the live line stop inheriting from the template once the clone is created
    • packages/billing/src/actions/contractWizardActions.ts now makes client_contracts.template_contract_id intent explicit for template-derived contracts: it is retained for draft/review provenance, while live recurring cadence semantics remain copied onto the contract lines themselves
    • server/src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts now adds T237, proving a cloned live contract line keeps cadence_owner: 'contract' and billing_timing: 'advance' even after the source template row is later mutated to client / arrears
    • validation for this checkpoint used cd server && npx vitest run src/test/unit/billing/templateLineCadenceOwner.persistence.test.ts --coverage.enabled false plus npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
    • npx tsc --pretty false --noEmit -p server/tsconfig.json is still blocked by an unrelated pre-existing type error at packages/billing/src/actions/creditActions.ts:1208 (Type 'IInvoice | null' is not assignable to type 'null'), so the checkpoint relies on the focused server unit seam rather than claiming a clean full-server compile
  • (2026-03-17) Pre-save recurring authoring previews now use one explicit copy contract for cadence-owner, timing, first-invoice, and partial-period outcomes, which closes F209, T238, and T239:

    • packages/billing/src/components/billing-dashboard/contracts/recurringAuthoringPreview.ts now defines the authoritative preview vocabulary for recurring authoring: cadence-owner label/summary, billing-timing label/summary, first-invoice behavior, and partial-period outcome
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/FixedFeeServicesStep.tsx now renders a Recurring Preview Before Save block for live contract authoring, so billing staff can see client-schedule versus contract-anniversary outcomes plus advance/arrears and partial-period consequences before saving
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/ReviewContractStep.tsx, packages/billing/src/components/billing-dashboard/contracts/template-wizard/steps/TemplateFixedFeeServicesStep.tsx, and packages/billing/src/components/billing-dashboard/contracts/template-wizard/steps/TemplateReviewContractStep.tsx now reuse the same preview helper instead of drifting into per-surface wording, which keeps template authoring and contract authoring aligned
    • packages/billing/tests/recurringAuthoringPreview.test.ts now proves both first-invoice families directly: client-cadence advance/arrears copy and contract-cadence advance/arrears copy
    • packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts and packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts now lock the UI wiring so both authoring surfaces continue to render the shared preview contract
    • validation for this checkpoint used cd server && npx vitest run ../packages/billing/tests/recurringAuthoringPreview.test.ts ../packages/billing/tests/contractWizardCadenceOwner.wiring.test.ts ../packages/billing/tests/templateWizardCadenceOwner.wiring.test.ts --coverage.enabled false plus npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • (2026-03-17) Unsupported recurring authoring combinations now have one explicit early-validation boundary in the wizards, which closes F210 and T240:

    • shared/billingClients/recurringAuthoringValidation.ts now defines the shared unsupported-combination message contract across line type, cadence owner, billing timing, and billing frequency; during the current rollout it names the exact blocked combination instead of only saying contract cadence is generically unavailable
    • packages/billing/src/components/billing-dashboard/contracts/ContractWizard.tsx now checks every present recurring family before finish/save-draft and surfaces the first unsupported combination message locally, so template-derived or resumed contract-cadence drafts fail before the action layer
    • packages/billing/src/components/billing-dashboard/contracts/template-wizard/TemplateWizard.tsx now applies that same boundary on the review/publish step, so template authoring surfaces reject unsupported contract-cadence combinations with the same detailed message contract used by the client wizard
    • packages/billing/tests/recurringAuthoringValidation.wiring.test.ts now proves both the detailed shared message shape (lineType, cadenceOwner, billingTiming, billingFrequency) and the fact that both wizard flows import and invoke the shared validator before persistence
    • validation for this checkpoint used cd server && npx vitest run ../packages/billing/tests/recurringAuthoringValidation.wiring.test.ts --coverage.enabled false plus npx tsc --pretty false --noEmit -p packages/billing/tsconfig.json
  • Related plans:
    • ee/docs/plans/2026-03-16-client-owned-contracts-simplification/
    • ee/docs/plans/2026-03-16-contract-template-normalization/
  • Pass-0 artifacts:
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PASS0_RECURRING_TIMING_APPENDIX.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/pass-0-source-inventory.json
  • Materialized-ledger artifacts:
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PERSISTED_SERVICE_PERIOD_RECORD.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_DUE_SELECTION.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_INVOICE_LINKAGE.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_PARITY_COMPARISON.md
    • ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/RECURRING_SERVICE_PERIOD_LIFECYCLE.md
  • Key runtime files:
    • packages/types/src/interfaces/recurringTiming.interfaces.ts
    • server/migrations/20260318120000_create_recurring_service_periods.cjs
    • server/migrations/20260318143000_add_invoice_linkage_to_recurring_service_periods.cjs
    • shared/billingClients/recurringServicePeriodDueSelection.ts
    • shared/billingClients/recurringServicePeriodKeys.ts
    • shared/billingClients/recurringServicePeriodLifecycle.ts
    • shared/billingClients/recurringServicePeriodInvoiceLinkage.ts
    • shared/billingClients/recurringServicePeriodParity.ts
    • packages/billing/src/lib/billing/billingEngine.ts
    • packages/billing/src/lib/billing/recurringTiming.ts
    • shared/billingClients/createBillingCycles.ts
    • shared/billingClients/clientCadenceServicePeriods.ts
    • shared/billingClients/contractCadenceServicePeriods.ts
    • shared/billingClients/recurrenceStorageModel.ts
    • shared/billingClients/recurringTiming.ts
    • packages/billing/src/actions/invoiceGeneration.ts
    • packages/billing/src/actions/recurringBillingRunActions.ts
    • packages/billing/src/repositories/accountingExportRepository.ts
  • Key UI/config files:
    • packages/billing/src/components/billing-dashboard/ContractLineDialog.tsx
    • packages/billing/src/components/billing-dashboard/contracts/ContractWizard.tsx
    • packages/billing/src/components/billing-dashboard/contracts/QuickStartGuide.tsx
    • packages/client-portal/src/actions/client-portal-actions/client-billing.ts
  • Key tests:
    • server/src/test/integration/billingInvoiceTiming.integration.test.ts
    • server/src/test/infrastructure/billing/invoices/*
    • server/src/test/infrastructure/billing/credits/*
    • server/src/test/unit/billing/recurringServicePeriodDueSelection.domain.test.ts
    • server/src/test/unit/billing/recurringServicePeriodInvoiceLinkage.domain.test.ts
    • server/src/test/unit/billing/recurringServicePeriodLifecycle.domain.test.ts
    • server/src/test/unit/billing/recurringServicePeriodParity.domain.test.ts
    • server/src/test/unit/migrations/recurringServicePeriodInvoiceLinkageMigration.test.ts
    • server/src/test/unit/migrations/recurringServicePeriodsMigration.test.ts
    • server/src/test/unit/billingEngine.test.ts
    • server/src/test/unit/billing/billingEngine.timing.test.ts

Open Questions

  • Which bucket/allowance behaviors must join v1 versus a follow-on?

  • Which exact service-period edit operations belong in v1 versus a follow-on?

  • (2026-03-18) Mixed-cadence portal/export coverage and header-vs-detail drift validation are now explicit, which closes T147 and T270:

    • packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx now covers a mixed invoice that contains both client-cadence and contract-cadence canonical recurring rows, so the portal detail dialog proves both shapes remain readable on one invoice instead of only exercising a single cadence path
    • server/src/test/integration/accounting/invoiceSelection.integration.test.ts now carries T147, proving export preview persistence remains stable when one invoice contains both a client-cadence recurring detail row and a contract-cadence recurring detail row; both persisted export lines keep service_period_source = canonical_detail_periods with their distinct canonical date ranges intact
    • server/src/test/unit/accounting/accountingExportValidation.servicePeriodProjection.test.ts now carries T270 on a focused validation seam instead of a migration-heavy integration harness: it proves AccountingExportValidation.ensureMappingsForBatch(...) marks the batch needs_attention and records service_period_projection_mismatch when stored export-line summary dates drift away from canonical invoice_charge_details
    • pass-0-source-inventory.json was refreshed in the same checkpoint because T003 inventory coverage must follow the live persisted-reader set; deleting the abandoned integration seam and adding the focused unit seam changed that list
    • the first DB-backed T270 attempt was intentionally discarded because it added migration churn without improving the contract under test; the focused unit seam exercises the validator directly and keeps the actual mismatch rule explicit instead of burying it under unrelated bootstrap noise
    • focused validation for this checkpoint used:
      • cd server && npx vitest run src/test/unit/accounting/accountingExportValidation.servicePeriodProjection.test.ts --coverage.enabled false
      • cd server && npx vitest run ../packages/client-portal/src/components/billing/InvoiceDetailsDialog.servicePeriods.test.tsx --coverage.enabled false
      • cd server && DB_HOST=127.0.0.1 DB_PORT=57433 DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=postpass123 DB_USER_SERVER=app_user DB_PASSWORD_SERVER=postpass123 npx vitest run src/test/integration/accounting/invoiceSelection.integration.test.ts -t "T147" --hookTimeout 600000 --coverage.enabled false