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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
66 KiB
66 KiB
Scratchpad — Service-Driven Invoicing Cutover
- Plan slug:
service-driven-invoicing-cutover - Created:
2026-03-18
What This Is
Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing this cutover plan.
Decisions
- (2026-03-20) Remove the last legacy billing-window selector shim from recurring generation internals and keep billing-cycle compatibility strictly at wrapper/action boundaries that convert to canonical selector-input windows.
- (2026-03-20) Feed invoice-candidate grouping with persisted scope metadata (
client_contract_id, PO scope, contract currency, client tax-source override) so split reasons and candidate partitioning align with real invoice constraints instead of row-only defaults. - (2026-03-20) Treat client-cadence windows as non-generateable when materialization gaps overlap the same client + invoice window, and enforce the same check during selector-input generation to block partial invoices.
- (2026-03-20) Keep
selectClientCadenceRecurringRunTargets(...)pagination metadata candidate-driven (total,totalPages) from the due-work reader response, even when client-cadence filtering reduces the returned run target count. - (2026-03-20) Cadence-source badge formatting in
AutomaticInvoicesmust be exhaustive and render explicit unknown-state copy for unexpected values rather than coercing unknowns toClient schedule. - (2026-03-20) Derive
AutomaticInvoicescontract context exclusively from candidate members and treat candidate-level contract fields as non-authoritative display metadata. - (2026-03-20) For grouped recurring invoice candidates (
memberCount > 1), disableAutomaticInvoicespreview entirely and require generation from the grouped candidate path so the UI never silently previews only the first member. - (2026-03-20) Keep this as the single active ALGA plan for service-driven invoicing cleanup; do not split remaining schema/caller/test cleanup into a separate plan folder.
- (2026-03-20) Treat post-drop
client_contract_linesremoval inpackages/clientsandpackages/client-portalas in-scope cutover completion work, not optional follow-up. - (2026-03-20) Prioritize strict candidate-first contracts over compatibility fallbacks in UI/action layers; missing or malformed due-work payloads should be treated as contract violations, not silently coerced.
- (2026-03-18) Treat this as a finishing-cutover plan layered on top of the broader
service-period-first-billing-and-cadence-ownershipplan, not a replacement for the whole architecture plan. - (2026-03-18) Scope includes all remaining app-level work necessary to make service-period-driven invoicing operationally true, including broader unfinished items that directly affect invoicing behavior.
- (2026-03-18) Hourly and usage are in scope for service-driven invoicing windows. They do not precompute charges, but they must bill available content inside the selected service period.
- (2026-03-18) The first cutover checkpoint should establish a shared due-work contract/builder layer in
shared/and@alga-psa/typesbefore changing any reader or UI code, so later steps reuse the same execution identity and display metadata. - (2026-03-18) For hourly/usage participation, widen the persisted
recurring_service_periods.charge_familycontract instead of introducing a family-agnostic shadow layer. The engine already evaluates content inside the selected service period; the missing cutover work was to let hourly/usage obligations exist honestly in the persisted service-period ledger and due-work contracts. - (2026-03-18) The minimal operator-facing management surface for this cutover is a dedicated Billing
Service Periodstab keyed byscheduleKey, backed by dry-run server actions for view/regeneration/repair. That keeps billing ops inside the main workflow while avoiding a larger standalone admin tool before the cutover is proven.
Discoveries / Constraints
- (2026-03-20)
calculateBillingForInvoiceWindow(...)andlegacy_client_cadence_windowidentity shims were only retained by unit tests; runtime recurring paths already execute through selector-input windows. - (2026-03-20) The shared candidate grouper already supported PO/currency/tax/export keys, but due-work readers were not populating those inputs from persisted DB rows, so financial-scope splits could collapse incorrectly.
- (2026-03-20) Partial materialization can surface persisted due rows for one recurring line while sibling eligible lines are missing; without explicit guards this still looked generateable and produced incomplete invoices.
- (2026-03-20) Recurring run target mapping was already candidate-shaped, but
selectClientCadenceRecurringRunTargets(...)still leaked target-count pagination side effects by recomputing totals from filtered targets. - (2026-03-20) Ready and history cadence badges were still using a binary
contract_anniversary/client_schedulebranch that mislabeled unknown values asClient schedule; this required an explicit unknown fallback formatter. - (2026-03-20) Candidate-level
contractNamecan hide partial member metadata loss; aggregating names from members and warning on incomplete member identity avoids collapsing mixed-validity candidates intoNo contract context. - (2026-03-20)
AutomaticInvoicespreview eligibility was keyed only to "one selected candidate", which still allowed grouped candidates and silently previewedmembers[0]; gating preview by single-member candidates closes that mismatch and keeps grouped candidates candidate-first. - (2026-03-20) Multi-agent audit identified remaining fallback risk in
AutomaticInvoices: grouped candidates can preview only the first member, and contract metadata rendering still trusts first-member-derived candidate fields. These are now explicitly tracked asF073/F074/F075. - (2026-03-20) Recurring run target mapping had a row-flatten legacy seam; mapper was partially tightened to candidate-first, but candidate-based paging/total semantics and coverage still need follow-through (
F076,T102,T103). - (2026-03-20) Full post-drop cleanup is incomplete outside billing package core:
packages/clientsandpackages/client-portalstill contain runtime/table references toclient_contract_linesand need migration toclient_contracts -> contracts -> contract_lines(F081,F082). - (2026-03-20) Additional live fallback seams remain around
template_contract_idusage in instantiated billing lookups and assignment queries (F083-F085), plus static guard coverage that currently misses non-server package tests (F086). - (2026-03-20) Plan artifacts now track remaining contract cleanup work: centralizing paginated due-work types in
@alga-psa/typesand finalizingbillingCycleIdsemantics on due-work rows (F087,F088). - (2026-03-18)
packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsxstill sources ready rows fromgetAvailableBillingPeriods(...), stores selection asSet<billing_cycle_id>, previews only bybilling_cycle_id, and maps PO-overage, generate, reverse, and delete flows through billing-cycle IDs. - (2026-03-18)
packages/billing/src/actions/billingAndTax.ts:getAvailableBillingPeriods(...)still joinsclient_billing_cyclesandinvoicesdirectly and has no service-period reader contract. - (2026-03-18)
packages/billing/src/actions/billingCycleActions.tsstill owns invoiced-history, reverse, and delete behavior, all throughclient_billing_cyclesplusinvoices.billing_cycle_id. - (2026-03-18)
packages/billing/src/actions/invoiceGeneration.tshas selector-input execution support internally, butpreviewInvoice(...)andgetPurchaseOrderOverageForBillingCycle(...)still only acceptbilling_cycle_id. - (2026-03-18)
server/src/lib/api/schemas/invoiceSchemas.tsstill definesgenerateInvoiceSchemaandinvoicePreviewRequestSchemastrictly in terms ofbilling_cycle_id. - (2026-03-18)
server/src/lib/api/services/InvoiceService.ts:generatePreview(...)still loadsclient_billing_cyclesdirectly and usescycle_idrather than a recurring execution-window contract. - (2026-03-18)
packages/billing/src/lib/billing/billingEngine.tsreadsrecurring_service_periodsfor due selection, but the application has almost no operator-facing read/write layer around those records. - (2026-03-18)
shared/billingClients/materializeClientCadenceServicePeriods.tsandshared/billingClients/materializeContractCadenceServicePeriods.tsexist as pure planning/materialization helpers, but the scan did not find a real app-level writer/replenishment flow that keeps due rows available for operations. - (2026-03-18)
server/migrations/20260318120000_create_recurring_service_periods.cjsconstrainscharge_familytofixed,product,license,bucket. That likely blocks a fully explicit hourly/usage materialization story unless widened or reframed. - (2026-03-18) The engine already supports selector-input recurring execution windows and can distinguish
billing_cycle_windowvscontract_cadence_window, so this plan should reuse that runtime path rather than invent another execution model. - (2026-03-18) Existing shared helpers in
shared/billingClients/recurringRunExecutionIdentity.tsalready provide deterministic execution identity, selection key, and retry key semantics. The new due-work builder can wrap those helpers rather than duplicating key-generation logic. - (2026-03-18)
server/src/test/test-utils/recurringTimingFixtures.tsalready has a persisted recurring service-period record fixture builder, which makes it straightforward to add contract-cadence due-work tests without standing up DB fixtures yet. - (2026-03-18) A due-work reader can safely merge persisted service-period rows with compatibility
client_billing_cyclesrows by deduping onexecutionIdentityKey, letting persisted canonical rows win while still surfacing legacy client-cadence work when no canonical row exists. - (2026-03-18) The current persisted-reader implementation can resolve
contract_lineandclient_contract_lineobligations directly fromrecurring_service_periods; other obligation types remain outside this first reader cut and therefore continue to rely on compatibility/fallback behavior. - (2026-03-18)
shared/billingClients/backfillRecurringServicePeriods.tswas already present and exported throughpackages/billing/src/index.ts; the missing work forF014was plan-specific validation that the zero-existing-records case is covered explicitly. - (2026-03-18)
shared/billingClients/recurringServicePeriodGenerationHorizon.tsalready models target horizon vs replenishment threshold coverage. The remaining gap forF015was explicit plan-level test coverage proving we report below-target horizon state before the low-water replenishment trigger trips. - (2026-03-18)
shared/billingClients/regenerateRecurringServicePeriods.tsand the existingbillingInvoiceTiming.integration.test.tsstaged-rollout scenario already implement regeneration semantics for untouched future rows while preserving edited/billed history. The remaining gap forF016was explicit unit coverage of the combined edited+billed override case in one regeneration pass. - (2026-03-18) Action-level integration coverage for
getAvailableRecurringDueWork(...)is practical with a fake transaction/query-builder harness because the reader logic is mostly SQL filtering/merging over server actions. This gives deterministic coverage for mixed-row sorting, pagination, search, and invoice-window date filtering without requiring the unavailable local Postgres harness. - (2026-03-18) A live local test Postgres container was already running on
127.0.0.1:57433withapp_user/postpass123;.env.localtestwas stale (5438+ old password). Focused DB-backed billing tests can run in this workspace by overriding the DB env vars inline instead of relying on the stale file values. - (2026-03-18) The persisted recurring timing contract originally blocked hourly/usage participation at the schema/type level even though the current billing engine worktree already resolves service-period timing for time and usage charges. Widening
RecurringChargeFamilyplus the migration constraint is the cutover step that lets those obligations be represented in due-work and persisted service-period ledgers. - (2026-03-18) The cleanest place to link billed recurring service periods back to persisted invoice detail rows is
packages/billing/src/services/invoiceService.ts, immediately after eachinvoice_charge_detailsinsert. That transaction already has the canonicalinvoice_id,item_id,item_detail_id, service-period dates, and config linkage needed for both bridged and unbridged recurring windows. - (2026-03-18) Local DB integration runs showed the test schema may not always include
client_contract_lines, even though production code can use it to map compatibility client-cadence linkage. The new linkage helper therefore degrades gracefully when that relation is absent and still links unbridged contract-cadence rows directly throughcontract_line_service_configuration.contract_line_id. - (2026-03-18) Persisted recurring service periods currently store canonical half-open service periods while invoice charge details still serialize an inclusive
servicePeriodEnddate in some paths. Matching linkage must therefore tolerate either the exact stored end date or the next-day exclusive representation to avoid missing otherwise-equivalent rows. - (2026-03-18)
hardDeleteInvoice(...)had an existing safeguard that blocked deleting any invoice with canonical recurring detail rows. The correct cutover behavior is narrower: keep blocking unlinked historical-detail invoices, but allow deletion when those canonical detail rows are also mirrored by persistedrecurring_service_periodslinkage that can be explicitly reopened. - (2026-03-18) Because persisted service periods do not currently remember whether a billed row was originally
generatedoredited, delete-repair now restores linked rows tolocked. That keeps them invoiceable for due selection while avoiding a false claim about the exact prior mutable state. - (2026-03-18) The safest first cut for missing-materialization diagnostics is to flag compatibility billing-cycle rows that survive the due-work merge only because no persisted client-cadence service-period row exists for the same recurring window. That produces actionable operator feedback without guessing about contract-cadence windows that may simply not be due yet.
- (2026-03-18)
pnpm exec tsxcan import the maintenance helpers directly fromshared/billingClients/*.ts, but importing throughpackages/billing/src/index.tspulls inpackages/dband currently fails under the repo's CJS transform because of top-level await inknexfile.ts. The cutover runbook should point operators at the direct shared-helper paths until a dedicated admin entrypoint exists. - (2026-03-18) The repo already had mixed-window selection-key determinism in
recurringBillingRunActionsthroughbuildRecurringRunSelectionIdentity(...); the remaining gap for cutover proof was the unbridged contract-cadence single-window case. That behavior is now explicitly locked by unit coverage instead of being implied by larger mixed-batch tests. - (2026-03-18) The local worktree already had the selector-input recurring-run plumbing in progress (
recurringBillingRunActions.ts,recurringBillingRunActions.shared.ts,invoiceGeneration.constants.ts, and their tests). TheAutomaticInvoicescutover can safely build on that by passing mixedtargetsinstead of forcing every selected row throughbillingCycleId. - (2026-03-18)
AutomaticInvoicesstill had a leftoverbillingCycleIdguard around preview selection even after the selector-input preview action existed. Contract-cadence preview support required removing that UI gate and storing preview state with a nullable bridge ID so the preview dialog can open for unbridged rows while generate-from-preview remains compatibility-only for now. - (2026-03-18) The PO-overage path had the same bridge-only split-brain as preview: batch selection only checked bridged rows and preview-dialog generation only worked for
billingCycleId. Fixing the next UI slice required adding a selector-input overage action, moving both batch and single-row overage checks ontoselectorInput, and teaching preview state to retainexecutionIdentityKeyplusselectorInputeven whenbillingCycleIdis null. - (2026-03-18)
previewInvoice(billing_cycle_id)still duplicated the selector-input preview builder after the UI cutover. The next backend cleanup re-routed legacy preview requests through one shared selector-input preview helper, widened preview error contracts withexecutionIdentityKey/nullablebillingCycleId, and attached the same context to selector-input generation validation failures so unbridged rows keep diagnostics without pretending to be billing-cycle-backed. - (2026-03-18) A small dedicated generation-success harness is enough to prove the selector-input generation contract without needing a full DB integration fixture. Mocking invoice insertion plus
Invoice.getFullInvoiceById(...)is sufficient to show that contract-cadence selector inputs persist invoices withbilling_cycle_id = null, while the legacygenerateInvoice('cycle-1')wrapper still inserts a bridged row and drives the same recurring selection path. - (2026-03-18)
F045needed one more backend cutover step beyond selector-input generation success: duplicate prevention for unbridged contract-cadence windows could not rely oninvoices.billing_cycle_id, so the guard now falls back torecurring_service_periods.invoice_idfor the same contract line and invoice window and still returns the standard duplicate error contract with nullablebillingCycleId. - (2026-03-18) The cleanest first cut for recurring invoiced history is invoice-centric, not billing-cycle-centric: aggregate
recurring_service_periodsperinvoice_id, fall back toinvoice_charge_detailsservice periods when persisted linkage is absent, and derive cadence/execution-window metadata from that summary while preserving a nullable bridgebilling_cycle_id. - (2026-03-18) Reverse/delete semantics for unbridged recurring invoices are now explicit in the operator layer: bridged rows continue through
removeBillingCycle(...)/hardDeleteBillingCycle(...), while unbridged rows route through invoice deletion and reopen linked recurring service periods without inventing a synthetic billing-cycle row. - (2026-03-18) History-label assertions in the DB-backed tests must normalize Postgres timestamp offsets before comparison. The reader returns correct ranges, but exact
T00:00:00.000Zstring matches are too strict for the integration harness because timestamps come back timezone-normalized. - (2026-03-18) The API cutover can avoid duplicating billing-cycle lookup logic by letting
InvoiceServicedelegate directly to the existing billing action wrappers: selector-input requests callpreviewInvoiceForSelectionInput(...)/generateInvoiceForSelectionInput(...), while compatibilitybilling_cycle_idrequests still callpreviewInvoice(...)/generateInvoice(...), which already route through the selector-aware pipeline internally. - (2026-03-18) Importing
ApiInvoiceControllerdirectly in a focused unit test currently drags an unrelated workspace alias failure (@alga-psa/product-extension-actionsvia UI/documents imports). The API cutover proof forT061-T064is therefore locked at theInvoiceServicecontract layer rather than the controller import graph. - (2026-03-18)
pnpm exec tsc --noEmit -p server/tsconfig.jsonstill fails in this workspace because of a pre-existing unrelated type error inpackages/billing/src/actions/creditActions.ts(IInvoice | nullassigned tonull). The recurring API slice compiles past its own new changes, but that broader server typecheck remains blocked until the existing billing worktree error is addressed. - (2026-03-18) The invoice list/filter cutover does not need a separate recurring read model yet. Extending
InvoiceService.buildBaseQuery(...)with a groupedrecurring_service_periodssummary join is enough to expose recurring execution metadata on list/detail responses and to make execution-window/cadence filters deterministic without breaking legacy invoices that still only have abilling_cycle_id. - (2026-03-18) The external invoice response contract previously rejected
billing_cycle_id: null, even though unbridged recurring invoices already exist. Widening the API schemas and shared invoice interfaces to allow nullable bridge IDs is the compatibility step that lets list/detail consumers accept service-driven recurring invoices without inventing fake cycle IDs. - (2026-03-18) The live DB-backed due-work reader had two integration-only gaps that unit harnesses did not catch:
fetchPersistedRecurringDueWorkDbRows(...)dropped all persisted rows when the optionalclient_contract_linesbranch hit a missing-table error, andmapPersistedRecurringDueWorkDbRowsToRows(...)still built selector identities from raw PostgresDateobjects. Fixing both made contract-cadence due rows visible again in the real ready-to-invoice workflow. - (2026-03-18) The new DB-backed happy-path tests for unbridged contract cadence need intentionally unaligned client billing cycles. If the client cycles line up with the contract cadence window, the due-work reader can legitimately surface a compatibility bridge and the test no longer proves the “no billing-cycle bridge” scenario.
- (2026-03-18) Hourly recurring charges are only eligible for billing when their
time_entriesstill point at a real ticket/project work item. A bare UUIDwork_item_idis ignored by the current billing-engine joins, so the service-driven hourly coverage needs a minimal valid ticket fixture even in focused integration tests. - (2026-03-18) Empty service-driven hourly/usage windows do not currently throw
Nothing to billwhen zero-dollar invoice suppression is off. The selector-input generation path creates a zero-dollar draft invoice with noinvoice_charges, so the plan coverage should lock that default behavior instead of forcing a rejection. - (2026-03-18) Mixed recurring invoices persist service families across two layers: fixed recurring content uses a consolidated
invoice_chargesparent plus canonicalinvoice_charge_details, while hourly and usage charges persist directly asinvoice_chargeswithout detail rows. Assertions that want the full mixed-family service set must combine those two persistence layers. - (2026-03-18) The shared domain contracts for operational view, governance, and regeneration conflicts were already sufficient for the first operator-management slice. The missing work for
F065-F068was application wiring: a permission-gated action layer, a tenant permission migration, and a lightweight Billing tab that can render those shared contracts and dry-run regeneration conflicts. - (2026-03-18)
recurringBillingRunActions.tsand the shared workflow event builders already emittedwindowIdentity,executionWindowKinds,selectionKey, andretryKey. The missing proof forF069was focused unit coverage that contract-only runs stay tagged ascontract_cadence_windowall the way through started/completed/failed payloads and that mixed runs keep deterministic keys. - (2026-03-18)
AutomaticInvoicesfailure mapping used nullish coalescing onperiod?.clientName, so an empty string suppressed the fallback toexecutionIdentityKeyand produced blank error labels for unbridged rows.F070required a small UI fix, not a broader logging change. - (2026-03-18) The existing cutover
RUNBOOK.mdalready contained the needed operator workflow once expanded with explicit cutover sequencing, migration checklist, and reverse/delete validation steps. The remaining gap forF071/F072was locking that content with a focused docs contract test. - (2026-03-18)
vitestruns this billing suite in randomized order, soinvoiceGeneration.preview.test.tsneeds to rebuild its default preview, PO, and selector-input mocks inbeforeEachrather than relying on initial hoisted implementations or one-time overrides from earlier tests. - (2026-03-18) Compatibility client-cadence due rows intentionally still submit recurring-run targets as
billingCycleIdplus the canonical execution window. Unlike unbridged contract-cadence rows, they do not need the fullselectorInputpayload to prove the legacy ready-table path still works. - (2026-03-18) The batch PO-overage allow option currently renders as
Allow overages (generate all invoices). Regression coverage has to assert that exact operator copy instead of a guessed label.
Commands / Runbooks
- (2026-03-20)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false(fromserver/; passed after adding grouped-preview gating coverage forT097/T098) - (2026-03-20)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false(fromserver/; passed after member-derived contract metadata warnings coverage forT099/T100) - (2026-03-20)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false(fromserver/; passed after exhaustive cadence-source fallback coverage forT101) - (2026-03-20)
pnpm exec vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled=false(fromserver/; passed after locking candidate-first target cardinality and candidate-page total semantics forT102/T103) - (2026-03-20)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T104|T105"(fromserver/; passed) - (2026-03-20)
pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts --coverage.enabled=false(fromserver/; passed after split-key grouping coverage forT106) - (2026-03-20)
pnpm exec vitest run src/test/unit/billing/invoiceGeneration.recurringSelection.test.ts src/test/unit/billing/recurringGenerationLegacyFallback.static.test.ts --coverage.enabled=false(fromserver/; passed after removing legacy billing-window recurring generation shims forT107) - (2026-03-18)
rg -n "billing_cycle_id" packages/billing/src/actions packages/billing/src/components server/src/lib/api packages/client-portal/src/actions -g '!**/*.test.*' - (2026-03-18)
rg -n "recurring_service_periods" packages/billing/src/actions packages/client-portal/src/actions server/src/lib/api -g '!**/*.test.*' - (2026-03-18)
sed -n '1,260p' packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx - (2026-03-18)
sed -n '428,535p' packages/billing/src/actions/invoiceGeneration.ts - (2026-03-18)
sed -n '260,520p' packages/billing/src/actions/billingCycleActions.ts - (2026-03-18)
sed -n '1720,1815p' server/src/lib/api/services/InvoiceService.ts - (2026-03-18)
python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/scaffold_plan.py "Service-Driven Invoicing Cutover" --slug service-driven-invoicing-cutover - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringDueWork.domain.test.ts src/test/unit/billing/recurringTiming.domain.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p tsconfig.json(fromserver/; broad compile did not return promptly during this checkpoint, so relied on targeted unit coverage instead) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringDueWork.domain.test.ts src/test/unit/billing/recurringServicePeriodDueSelection.domain.test.ts src/test/unit/billing/recurringTiming.domain.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p tsconfig.json(frompackages/billing/; passed after fixing one implicit-anyinbillingAndTax.ts) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringServicePeriodGenerationHorizon.domain.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringServicePeriodRegeneration.domain.test.ts src/test/unit/billing/recurringServicePeriodRegenerationConflict.domain.test.ts src/test/unit/billing/recurringServicePeriodGenerationHorizon.domain.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T316/T323/T324/T327"(fromserver/; blocked in this environment because Postgres on127.0.0.1:5438refused connections) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec tsx --eval "import { backfillRecurringServicePeriods } from './shared/billingClients/backfillRecurringServicePeriods.ts'; import { assessRecurringServicePeriodGenerationCoverage } from './shared/billingClients/recurringServicePeriodGenerationHorizon.ts'; import { regenerateRecurringServicePeriods } from './shared/billingClients/regenerateRecurringServicePeriods.ts'; console.log(JSON.stringify({ backfill: typeof backfillRecurringServicePeriods, coverage: typeof assessRecurringServicePeriodGenerationCoverage, regenerate: typeof regenerateRecurringServicePeriods }));"(from repo root; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringTiming.domain.test.ts -t "T023" --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx src/test/unit/billing/invoiceGeneration.preview.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx src/test/unit/billing/invoiceGeneration.preview.test.ts src/test/unit/api/invoiceRecurringList.contract.test.ts src/test/unit/billing/recurringServicePeriodBackfill.domain.test.ts src/test/unit/billing/manualInvoiceActions.recurringIsolation.test.ts --coverage.enabled=false(fromserver/; passed after tightening randomized-order mock resets ininvoiceGeneration.preview.test.tsand aligning the compatibility regression assertions with the current client-cadence target shape and PO dialog labels) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts src/test/unit/billing/invoiceGeneration.duplicate.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/billing/tsconfig.json(from repo root; passed after fixing one staleinvoiceService.tstype assertion and the renamedAutomaticInvoicesdue-work fields) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/types/tsconfig.json(from repo root; passed) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T316/T323/T324/T327"(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringDueWork.domain.test.ts src/test/unit/migrations/recurringServicePeriodsMigration.test.ts ../packages/types/src/recurringServicePeriodRecord.typecheck.test.ts(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/types/tsconfig.json(from repo root; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/invoiceService.fixedPersistence.test.ts(fromserver/; passed after adding a no-op guard for thin transaction mocks that do not expose Knex query-builder chaining) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T019"(fromserver/; passed) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T020"(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run ../packages/billing/tests/invoiceModification.recurringDeletionGuard.test.ts(fromserver/; passed and confirmed the legacy unlinked canonical-detail delete guard still holds) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T021"(fromserver/; passed) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts(fromserver/; passed after narrowing diagnostics to compatibility rows that survive merge without a persistedrecordId) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T021|T022|T051|T052|T054|T055|T056|T057"(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/billing/tsconfig.json(from repo root; passed for the recurring-history/reverse/delete checkpoint) - (2026-03-18)
pnpm exec vitest run src/test/unit/api/invoiceRecurringSelectorInput.schema.test.ts src/test/unit/api/invoiceService.recurringSelectorInput.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p server/tsconfig.json(from repo root; still fails on a pre-existing unrelatedpackages/billing/src/actions/creditActions.tstype error after the recurring API changes typechecked cleanly) - (2026-03-18)
pnpm exec vitest run src/test/unit/api/invoiceRecurringSelectorInput.schema.test.ts src/test/unit/api/invoiceService.recurringSelectorInput.test.ts src/test/unit/api/invoiceRecurringList.contract.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/types/tsconfig.json(from repo root; passed after adding recurring execution metadata to shared invoice interfaces) - (2026-03-18)
pnpm exec tsc --noEmit -p server/tsconfig.json(from repo root; still fails only on the pre-existing unrelatedpackages/billing/src/actions/creditActions.tstype error) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringServicePeriodActions.test.ts src/test/unit/billing/recurringServicePeriodsTab.ui.test.tsx --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/billing/tsconfig.json(from repo root; passed for the recurring service-period management checkpoint) - (2026-03-18)
pnpm exec vitest run src/test/unit/billing/recurringBillingRunWindowIdentity.test.ts src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx src/test/unit/docs/serviceDrivenInvoicingCutover.runbook.test.ts --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
pnpm exec tsc --noEmit -p packages/billing/tsconfig.json(from repo root; passed for the workflow-event, execution-identity fallback, and runbook checkpoint) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T069|T070|T071|T072|T073"(fromserver/; passed after switching hourly fixtures to real ticket work items and aligning mixed/empty metered assertions with the current persistence and zero-dollar invoice behavior) - (2026-03-18)
pnpm exec vitest run src/test/unit/billingEngine.test.ts -t "T074" --coverage.enabled=false(fromserver/; passed) - (2026-03-18)
DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T086|T087|T088|T089|T090"(fromserver/; passed after hardening persisted due-work row fetching/normalization and aligning the contract-cadence fixtures so they were truly unbridged)
Links / References
- Broad architecture plan:
- Key files:
- AutomaticInvoices.tsx
- billingAndTax.ts
- billingCycleActions.ts
- invoiceGeneration.ts
- billingEngine.ts
- invoiceSchemas.ts
- InvoiceService.ts
- materializeClientCadenceServicePeriods.ts
- materializeContractCadenceServicePeriods.ts
- recurringDueWork.ts
- recurringTiming.interfaces.ts
- recurringDueWork.domain.test.ts
- billingAndTax.ts
- recurringServicePeriodDueSelection.domain.test.ts
- backfillRecurringServicePeriods.ts
- recurringServicePeriodBackfill.domain.test.ts
- recurringServicePeriodActions.ts
- RecurringServicePeriodsTab.tsx
- 20260318194500_add_recurring_service_period_permissions.cjs
- recurringServicePeriodActions.test.ts
- recurringServicePeriodsTab.ui.test.tsx
- recurringBillingRunWindowIdentity.test.ts
- serviceDrivenInvoicingCutover.runbook.test.ts
Completed Checkpoints
- (2026-03-20) Completed
F079andT107by removing legacy billing-window recurring generation helper paths (calculateBillingForInvoiceWindow(...)and legacy selector identity shims), migrating recurring-selection unit coverage to canonical selector-input calls, and adding a static runtime source guard that blocks reintroduction of legacy fallback builders. - (2026-03-20) Completed
F078andT106by wiring persisted grouping metadata into recurring due-work candidate partitioning (clientContractId,purchaseOrderScopeKey,currencyCode,taxSource) so same-window due selections split according to real financial/PO constraints and surface deterministic split reasons. - (2026-03-20) Completed
F077andT104/T105by blocking generation for partially materialized client-cadence windows at both due-work and invoice-generation layers: due-work candidates now flipcanGenerate=falsewith an explicit repair reason when overlapping materialization gaps exist, and selector-input generation now asserts all active client-cadence recurring lines in the window have non-archived persisted service-period rows before invoicing. - (2026-03-20) Completed
F076andT102/T103by preserving one-target-per-candidate mapping for client cadence candidates and switchingselectClientCadenceRecurringRunTargets(...)to return paging totals from the due-work candidate response instead of recomputing totals from filtered target count. - (2026-03-20) Completed
F075andT101by introducing exhaustive cadence-source badge formatting forAutomaticInvoicesready/history rows and rendering explicit unknown-state copy (Unknown cadence source (...)) instead of defaulting unexpected cadence-source values toClient schedule. - (2026-03-20) Completed
F074andT099/T100by deriving contract and contract-line display from candidate members, preserving visible member-derived contract context even when metadata is partially missing, and surfacing explicitContract metadata missingwarning copy keyed to the affected candidate. - (2026-03-20) Completed
F073andT097/T098by restrictingAutomaticInvoicespreview to single-member candidates, rendering explicit grouped-preview-unavailable copy when a grouped candidate is selected, and adding UI tests that lock both the disabled grouped behavior and the preserved single-member preview selector-input path. - (2026-03-18) Completed
F001,F002,F005,F006, andF007by addingIRecurringDueWorkRowplusbuildClientScheduleDueWorkRow(...)/buildServicePeriodRecurringDueWorkRow(...). The builders now normalize row identity, cadence-source metadata, service-period labels, invoice-window labels, and contract context on top of the existing recurring execution-window identity helpers. - (2026-03-18) Completed
T001throughT005with focused server-side unit coverage proving stable client/contract execution identities and the new display/context fields on due-work rows. - (2026-03-18) Completed
F003,F004,F008,F009,F010,F011,F012, andF013by addinggetAvailableRecurringDueWork(...)inbillingAndTax.ts. The reader now pulls ready persisted rows fromrecurring_service_periods, carries schedule/period keys and due-state metadata into due-work rows, allows unbridged contract-cadence windows, and merges compatibility billing-cycle rows underneath canonical rows by execution identity. - (2026-03-18) Completed
T006,T007, andT008with unit coverage for billed/archived/superseded suppression and compatibility-row merge behavior when canonical persisted rows are absent. - (2026-03-18) Completed
F014/T013by validating the pre-existingbackfillRecurringServicePeriods(...)support with an explicit zero-existing-records test. Active recurring obligations can now be asserted to backfill into future generated service-period rows before the UI depends on them. - (2026-03-18) Completed
F015/T014by validating the pre-existing generation-horizon coverage model with an explicit replenishment test. The system now has plan-specific proof that future service periods can be assessed against both the target horizon and the lower replenishment threshold without conflating those two states. - (2026-03-18) Completed
F016/T015by validating the pre-existing regeneration planner with an explicit edited-plus-billed override preservation test. Future untouched rows can now be regenerated while edited and billed slots remain preserved rather than being silently overwritten. - (2026-03-18) Completed
T009throughT012with an action-level due-work reader harness that exercises the realgetAvailableRecurringDueWork(...)logic. Mixed client/contract rows now have explicit coverage for deterministic ordering, pagination stability, search by client name without a billing-cycle bridge, and invoice-window-based date filtering. - (2026-03-18) Completed
T016by running the existing DB-backed staged-rollout regeneration scenario against the live local Postgres container with inline DB env overrides. The integration path now has verified proof that regeneration updates future rows while preserving edited and billed history. - (2026-03-18) Completed
F017,F018,F019,T017, andT018by widening the shared recurring charge-family contract and therecurring_service_periodsmigration constraint to includehourlyandusage, then proving those persisted rows flow into due-work selection. The chosen cutover model is explicit ledger support for hourly/usage obligations, not a parallel family-agnostic abstraction. - (2026-03-18) Completed
F020,T019, andT020by linking persistedrecurring_service_periodsrows to the exactinvoice_charge_detailsrows created during invoice persistence for fixed, product, and license recurring charges. The linkage now marks matching service periods asbilled, stores invoice/item/detail IDs plusinvoice_linked_at, and has focused DB-backed proof for both bridged client-cadence and unbridged contract-cadence invoice creation paths. - (2026-03-18) Completed
F021andT021by repairing recurring service-period linkage during invoice hard-delete for linked recurring invoices. Deletion now clearsinvoice_id/ charge linkage columns, restores affected rows tolocked, and makes the same unbridged contract-cadence service period invoiceable again, while preserving the legacy hard-delete guard for invoices that only have canonical detail rows and no persisted service-period linkage. - (2026-03-18) Completed
F022by addingmaterializationGapsdiagnostics togetAvailableRecurringDueWork(...). The due-work reader now reports when a client-cadence recurring window is only surfacing through the legacy billing-cycle compatibility path because persisted recurring service periods were not materialized for that window, giving operator surfaces an explicit explanation instead of a silent fallback. - (2026-03-18) Completed
F023andT075by addingRUNBOOK.mdfor the service-driven invoicing cutover and tightening the due-work reader harness around the missing-materialization signal. Billing operators and developers now have one concrete maintenance entrypoint for coverage assessment, backfill, regeneration, and reverse/delete validation, and the plan includes executablepnpm exec tsxhelper invocations plus focused due-work reader verification. - (2026-03-18) Completed
F024andT023by locking the unbridged contract-cadence duplicate-prevention identity ontobuildRecurringRunSelectionIdentity(...). The selector-input path now has explicit proof that duplicate-prevention keys stay stable even when nobilling_cycle_idbridge exists and the same execution window is rebuilt or deduplicated repeatedly. - (2026-03-18) Completed
F025throughF032andT025throughT031by moving theAutomaticInvoicesready table ontogetAvailableRecurringDueWork(...), swapping selection onto execution identities, rendering cadence/service-period/invoice-window/contract metadata, and generating mixed client-cadence plus contract-cadence batches through recurring-runtargets. The UI now maps execution-identity failures back to the rendered contract row even whenbilling_cycle_idis null, while preserving the existing preview and PO-overage compatibility path for bridged rows until the selector-input preview work lands. - (2026-03-18) Completed
F033,F041, andT032/T033/T041/T042/T043by introducingpreviewInvoiceForSelectionInput(...), wiringAutomaticInvoicespreview selection toselectorInput, and validating parity plus permission behavior across client-cadence and contract-cadence rows. The ready table can now open previews for unbridged contract-cadence rows, while the preview dialog still keeps generate-from-preview disabled until the later selector-input generation cutover is implemented. - (2026-03-18) Completed
F034throughF040,F042, andT034/T035/T036/T037/T038/T039/T040/T044/T045by moving batch and preview-dialog generation fully onto selector-aware recurring-runtargets, addinggetPurchaseOrderOverageForSelectionInput(...), carrying execution identity through preview state and single-row PO confirmation, and surfacing an explicitNo billing cycle bridgebadge for unbridged contract-cadence rows. Compatibility client-cadence rows still render and generate through the same UI, but the UI no longer depends on a bridge ID to preview, warn, or generate recurring due work. - (2026-03-18) Completed
F043,F046,F047,F048, andT048/T049/T050by extracting a shared selector-input preview builder, routing legacypreviewInvoice(billing_cycle_id)through it, preserving canonical recurring detail rows for both cadence sources, and attaching execution-window context to selector-input preview/generation validation failures. Selector-input recurring generation still enforces billing email and PO-required contract validation, but unbridged rows now keepexecutionIdentityKeyplus nullablebillingCycleIdin those failures instead of collapsing diagnostics onto a fake bridge key. - (2026-03-18) Completed
F044andT046/T047by locking successful selector-input generation into a first-class public contract. The generation action now has focused coverage proving that an unbridged contract-cadence selector input creates an invoice withbilling_cycle_id = null, while the compatibilitygenerateInvoice('cycle-1')wrapper still routes through the same selector-driven recurring selection path and creates the bridged legacy invoice row. - (2026-03-18) Completed
F045andT024by extending duplicate prevention to unbridged contract-cadence selector inputs. The generation action now checks linkedrecurring_service_periodswhen no bridgebilling_cycle_idexists and still returns the standard duplicate error contract with execution identity plus nullable bridge metadata. - (2026-03-18) Completed
F049throughF056andT022/T051/T052/T053/T054/T055/T056/T057/T058by replacing billing-cycle-centric invoiced history with an invoice-centric recurring history reader, surfacing cadence/service-period/invoice-window metadata in the UI, and splitting reverse/delete behavior between bridged compatibility rows and unbridged service-period-backed rows. Operators can now review recurring invoice history without requiringbilling_cycle_id, and the action copy makes it explicit when reversing or deleting will reopen linked recurring service periods instead of acting through a client billing cycle. - (2026-03-18) Completed
F057throughF060,F063,F064, andT059/T060/T061/T062/T063/T064/T065by extending the external API preview/generate request schemas to acceptselector_input, routingInvoiceServiceonto the selector-aware billing action wrappers, and preserving compatibilitybilling_cycle_idcallers through the same internal selector-input pipeline. The API layer no longer reimplements preview lookup againstclient_billing_cyclesfor unbridged contract-cadence requests. - (2026-03-18) Completed
F061,F062, andT066/T067/T068by extending invoice list/detail contracts with recurring execution metadata, allowing nullablebilling_cycle_idin the API response schemas, and adding execution-window/cadence filters on top of a groupedrecurring_service_periodssummary join insideInvoiceService. Existing consumers can now parse service-driven recurring invoices without a billing-cycle bridge while still filtering the invoice list by recurring cadence semantics. - (2026-03-18) Completed
F065throughF068andT076/T077/T078/T079/T080by addingrecurringServicePeriodActions.ts, a newService Periodsbilling tab, and a tenant permission migration forbilling.recurring_service_periods. Billing ops can now load one persisted schedule byscheduleKey, inspect generated/edited/billed rows with governance metadata, and preview regeneration conflicts through a permission-gated operator action instead of relying only on the runbook and raw helper scripts. - (2026-03-18) Completed
F069throughF072andT081/T082/T083/T084/T085by locking recurring billing run workflow payloads to explicit contract-vs-mixed window identities, fixingAutomaticInvoiceserror-label fallback to useexecutionIdentityKeywhen a row has no usable client label, and contract-testing the expanded cutover runbook. Operators and support now get deterministic event diagnostics for contract-cadence runs, UI errors no longer collapse to blank labels for unbridged rows, and the migration/backfill/reverse-delete sequence is documented as an enforceable artifact rather than informal notes. - (2026-03-18) Completed
T069throughT073with focused DB-backed metered recurring coverage inbillingInvoiceTiming.integration.test.ts. Contract-cadence hourly and usage lines now have proof that they bill only records inside the selected service period, empty metered windows follow the current zero-dollar draft path without inventing line items, and mixed recurring invoices can persist fixed, hourly, and usage content under one execution window when assertions read the correct parent/detail persistence layers. - (2026-03-18) Completed
T074by adding focused unit coverage inbillingEngine.test.tsfor explicit service-driven metered timing. The billing engine now has locked proof that hourly and usage charge rows preserve canonicalservicePeriodStart,servicePeriodEnd, andbillingTimingmetadata when calculated from selector-input service periods. - (2026-03-18) Completed
T086throughT090with DB-backed recurring happy-path coverage inbillingInvoiceTiming.integration.test.ts. The integration suite now proves end-to-end due-work selection, preview, generate, history, mixed-batch generation, contract delete reappearance, and bridged client reverse reappearance, whilebillingAndTax.tsnow degrades gracefully when optional client-contract-line tables are absent and normalizes persisted due-work identity keys from live Postgres date values before building selector inputs. - (2026-03-18) Completed
T091throughT096with the last regression checkpoint. Compatibility client-cadence rows still preview and batch-generate throughAutomaticInvoices, legacy PO-overage behavior still routes through selector-input identity without changing the operator flow, compatibility preview errors keep their historical invalid/unauthorized shape,InvoiceService.buildBaseQuery(...)still selects recurring summary fields for list/detail readers, rerunning service-period backfill stays idempotent once the future horizon exists, and non-recurring manual prepayment invoice creation remains isolated from recurring service-period plumbing.
Latest Updates (2026-03-20)
T108candidate-level due-work assertions were initially flaky when relying on implicit compatibility rows; making the test materialize one deterministic client-cadence recurring service-period record and asserting candidate/member contracts directly removed row-flatten dependence while staying stable under partial-materialization guards.DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T108"(fromserver/; passed after making candidate discovery deterministic and avoiding over-strictcanGenerateexpectations)- Completed
F080andT108by refactoring recurring due-work integration assertions to validate candidate-level contracts directly inbillingInvoiceTiming.integration.test.tswithout relying oninvoiceCandidates.flatMap(...)row helpers for the new coverage path. - The legacy
packages/clients/src/models/clientContractLine.tspath still queriedclient_contract_linesand could throw UUID filter errors whenservice_categorywas absent in post-drop data. The model/action were migrated to contract-owned joins and nullable-category overlap handling so legacy admin actions remain safe on dropped schemas. DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/clientBillingConfiguration.postDrop.integration.test.ts(fromserver/; passed)- Completed
F081andT109by migrating@alga-psa/clientsadmin billing configuration reads/updates offclient_contract_lines, adding DB-backed proof that post-drop client billing actions load and mutate recurring contract-line settings without touching the dropped table. packages/client-portal/src/actions/account.ts:getActiveServices(...)still had post-drop schema drift while migrating away fromclient_contract_lines: it selectedservice_catalog.service_typeandcontract_line_service_bucket_config.total_hours, but live post-drop schema exposescontract_lines.contract_line_typeandcontract_line_service_bucket_config.total_minutes.DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/clientPortalBillingReads.postDrop.integration.test.ts(fromserver/; passed after aligning client-portal account/billing reads to post-drop columns and client-contract joins)pnpm exec vitest run --coverage.enabled=false ../packages/client-portal/src/actions/client-portal-actions/client-billing.bucketPeriods.test.ts(fromserver/; passed after updating the mocked post-drop join alias)- Completed
F082andT110by migrating client-portal billing/account/services reads fromclient_contract_linesto the post-drop contract chain (client_contracts -> contracts -> contract_lines) and adding DB-backed proof that active recurring service rendering and billing cycle reads no longer issue legacy table queries. - Live billing still had two template-provenance join fallbacks after the post-drop contract migration:
BillingEngine.fetchDiscounts(...)joinedclient_contractsby(template_contract_id OR contract_id), andbucketUsageService.calculatePeriod(...)joinedcontractsviacoalesce(cc.template_contract_id, cc.contract_id). DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/billingInvoiceTiming.integration.test.ts -t "T111:"(fromserver/; passed)DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/bucketUsageIntegration.test.ts(fromserver/; skipped by suite guards in this environment)pnpm exec vitest run --coverage.enabled=false src/test/unit/bucketUsageService.test.ts(fromserver/; skipped by suite guards in this environment)- Completed
F083andT111by removing template fallback joins from live billing computation paths (billingEngine.tsandbucketUsageService.ts) and adding DB-backed integration proof that billing-engine SQL uses canonicalcc.contract_id = c.contract_idjoins without template fallback expressions. - Contract assignment reads in
contractActions.tsstill accepted template IDs on instantiated query paths (getContractAssignments(...)and assignment counting ingetContractSummary(...)) viaorWhere template_contract_id, which blurred template-detail and instantiated-detail responsibilities post-drop. DB_HOST=127.0.0.1 DB_PORT=57433 DB_NAME=server DB_NAME_SERVER=server DB_USER_SERVER=app_user DB_USER_ADMIN=postgres DB_PASSWORD_SERVER=postpass123 DB_PASSWORD_ADMIN=postpass123 pnpm exec vitest run --coverage.enabled=false src/test/integration/contractAssignmentLookup.postDrop.integration.test.ts(fromserver/; passed)- Completed
F084andT112by splitting instantiated assignment loading from template detail loading: assignment readers now resolve only byclient_contracts.contract_id, while template detail remains available through the template-aware summary path (contract_templates+contract_template_lines) without leaking template IDs into instantiated assignment queries. ContractLineService.assignPlanToClient(...)still executed a runtime side-effect write that backfilledclient_contracts.template_contract_idwhenever the field was null, even though template provenance is now optional metadata and not a live assignment requirement.pnpm exec vitest run --coverage.enabled=false src/test/unit/api/contractLineService.clientOwnedMutation.test.ts(fromserver/; passed)- Completed
F085andT113by removingtemplate_contract_idbackfill writes fromContractLineServiceassignment flow and adding focused unit coverage that assignment cloning still works while proving noclient_contractsmutation occurs when template provenance is absent. pnpm exec vitest run --coverage.enabled=false src/test/unit/billing/clientContractLineRuntimeSourceGuards.static.test.ts(fromserver/; passed)- Completed
F086andT114by extending the post-drop static runtime hygiene guard to scanpackages/client-portal/src/actionsandpackages/clientsruntime sources (actions + models) for forbiddenclient_contract_lines/client_contract_servicestable usage. packages/billing/src/actions/billingAndTax.tsstill owned local paginated due-work/materialization-gap interfaces;AutomaticInvoices.tsximported the gap type from billing actions instead of the shared@alga-psa/typescontract.pnpm exec vitest run --coverage.enabled=false src/test/unit/billing/recurringDueWorkTypeContracts.static.test.ts(fromserver/; passed)pnpm exec tsc --noEmit -p packages/types/tsconfig.json(from repo root; passed)pnpm exec vitest run --coverage.enabled=false src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx(fromserver/; passed)pnpm exec tsc --noEmit -p packages/billing/tsconfig.json(from repo root; still fails on pre-existing unrelated worktree type errors outside this F087/F088 slice)- Completed
F087,F088,T115, andT116by centralizing paginated recurring due-work and materialization-gap interfaces in@alga-psa/types, consuming those shared contracts in billing actions/UI, and explicitly taggingbillingCycleIdas deprecated legacy bridge metadata in canonical due-work type contracts.
Open Questions
- Should the first cut of the UI show client-cadence and contract-cadence due rows in one unified table or grouped sections?
- Should hourly/usage participation widen
recurring_service_periods.charge_family, or should the due-work reader treat service periods as cadence windows independent of eventual charge-family projection? - What exact historical repair semantics should reverse/delete use for billed recurring service periods with no
billing_cycle_idbridge?