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

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 AutomaticInvoices must be exhaustive and render explicit unknown-state copy for unexpected values rather than coercing unknowns to Client schedule.
  • (2026-03-20) Derive AutomaticInvoices contract 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), disable AutomaticInvoices preview 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_lines removal in packages/clients and packages/client-portal as 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-ownership plan, 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/types before 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_family contract 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 Periods tab keyed by scheduleKey, 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(...) and legacy_client_cadence_window identity 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_schedule branch that mislabeled unknown values as Client schedule; this required an explicit unknown fallback formatter.
  • (2026-03-20) Candidate-level contractName can hide partial member metadata loss; aggregating names from members and warning on incomplete member identity avoids collapsing mixed-validity candidates into No contract context.
  • (2026-03-20) AutomaticInvoices preview eligibility was keyed only to "one selected candidate", which still allowed grouped candidates and silently previewed members[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 as F073/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/clients and packages/client-portal still contain runtime/table references to client_contract_lines and need migration to client_contracts -> contracts -> contract_lines (F081, F082).
  • (2026-03-20) Additional live fallback seams remain around template_contract_id usage 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/types and finalizing billingCycleId semantics on due-work rows (F087, F088).
  • (2026-03-18) packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx still sources ready rows from getAvailableBillingPeriods(...), stores selection as Set<billing_cycle_id>, previews only by billing_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 joins client_billing_cycles and invoices directly and has no service-period reader contract.
  • (2026-03-18) packages/billing/src/actions/billingCycleActions.ts still owns invoiced-history, reverse, and delete behavior, all through client_billing_cycles plus invoices.billing_cycle_id.
  • (2026-03-18) packages/billing/src/actions/invoiceGeneration.ts has selector-input execution support internally, but previewInvoice(...) and getPurchaseOrderOverageForBillingCycle(...) still only accept billing_cycle_id.
  • (2026-03-18) server/src/lib/api/schemas/invoiceSchemas.ts still defines generateInvoiceSchema and invoicePreviewRequestSchema strictly in terms of billing_cycle_id.
  • (2026-03-18) server/src/lib/api/services/InvoiceService.ts:generatePreview(...) still loads client_billing_cycles directly and uses cycle_id rather than a recurring execution-window contract.
  • (2026-03-18) packages/billing/src/lib/billing/billingEngine.ts reads recurring_service_periods for due selection, but the application has almost no operator-facing read/write layer around those records.
  • (2026-03-18) shared/billingClients/materializeClientCadenceServicePeriods.ts and shared/billingClients/materializeContractCadenceServicePeriods.ts exist 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.cjs constrains charge_family to fixed, 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_window vs contract_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.ts already 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.ts already 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_cycles rows by deduping on executionIdentityKey, 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_line and client_contract_line obligations directly from recurring_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.ts was already present and exported through packages/billing/src/index.ts; the missing work for F014 was plan-specific validation that the zero-existing-records case is covered explicitly.
  • (2026-03-18) shared/billingClients/recurringServicePeriodGenerationHorizon.ts already models target horizon vs replenishment threshold coverage. The remaining gap for F015 was 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.ts and the existing billingInvoiceTiming.integration.test.ts staged-rollout scenario already implement regeneration semantics for untouched future rows while preserving edited/billed history. The remaining gap for F016 was 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:57433 with app_user/postpass123; .env.localtest was 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 RecurringChargeFamily plus 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 each invoice_charge_details insert. That transaction already has the canonical invoice_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 through contract_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 servicePeriodEnd date 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 persisted recurring_service_periods linkage that can be explicitly reopened.
  • (2026-03-18) Because persisted service periods do not currently remember whether a billed row was originally generated or edited, delete-repair now restores linked rows to locked. 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 tsx can import the maintenance helpers directly from shared/billingClients/*.ts, but importing through packages/billing/src/index.ts pulls in packages/db and currently fails under the repo's CJS transform because of top-level await in knexfile.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 recurringBillingRunActions through buildRecurringRunSelectionIdentity(...); 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). The AutomaticInvoices cutover can safely build on that by passing mixed targets instead of forcing every selected row through billingCycleId.
  • (2026-03-18) AutomaticInvoices still had a leftover billingCycleId guard 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 onto selectorInput, and teaching preview state to retain executionIdentityKey plus selectorInput even when billingCycleId is 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 with executionIdentityKey/nullable billingCycleId, 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 with billing_cycle_id = null, while the legacy generateInvoice('cycle-1') wrapper still inserts a bridged row and drives the same recurring selection path.
  • (2026-03-18) F045 needed one more backend cutover step beyond selector-input generation success: duplicate prevention for unbridged contract-cadence windows could not rely on invoices.billing_cycle_id, so the guard now falls back to recurring_service_periods.invoice_id for the same contract line and invoice window and still returns the standard duplicate error contract with nullable billingCycleId.
  • (2026-03-18) The cleanest first cut for recurring invoiced history is invoice-centric, not billing-cycle-centric: aggregate recurring_service_periods per invoice_id, fall back to invoice_charge_details service periods when persisted linkage is absent, and derive cadence/execution-window metadata from that summary while preserving a nullable bridge billing_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.000Z string 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 InvoiceService delegate directly to the existing billing action wrappers: selector-input requests call previewInvoiceForSelectionInput(...) / generateInvoiceForSelectionInput(...), while compatibility billing_cycle_id requests still call previewInvoice(...) / generateInvoice(...), which already route through the selector-aware pipeline internally.
  • (2026-03-18) Importing ApiInvoiceController directly in a focused unit test currently drags an unrelated workspace alias failure (@alga-psa/product-extension-actions via UI/documents imports). The API cutover proof for T061-T064 is therefore locked at the InvoiceService contract layer rather than the controller import graph.
  • (2026-03-18) pnpm exec tsc --noEmit -p server/tsconfig.json still fails in this workspace because of a pre-existing unrelated type error in packages/billing/src/actions/creditActions.ts (IInvoice | null assigned to null). 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 grouped recurring_service_periods summary 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 a billing_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 optional client_contract_lines branch hit a missing-table error, and mapPersistedRecurringDueWorkDbRowsToRows(...) still built selector identities from raw Postgres Date objects. 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_entries still point at a real ticket/project work item. A bare UUID work_item_id is 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 bill when zero-dollar invoice suppression is off. The selector-input generation path creates a zero-dollar draft invoice with no invoice_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_charges parent plus canonical invoice_charge_details, while hourly and usage charges persist directly as invoice_charges without 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-F068 was 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.ts and the shared workflow event builders already emitted windowIdentity, executionWindowKinds, selectionKey, and retryKey. The missing proof for F069 was focused unit coverage that contract-only runs stay tagged as contract_cadence_window all the way through started/completed/failed payloads and that mixed runs keep deterministic keys.
  • (2026-03-18) AutomaticInvoices failure mapping used nullish coalescing on period?.clientName, so an empty string suppressed the fallback to executionIdentityKey and produced blank error labels for unbridged rows. F070 required a small UI fix, not a broader logging change.
  • (2026-03-18) The existing cutover RUNBOOK.md already contained the needed operator workflow once expanded with explicit cutover sequencing, migration checklist, and reverse/delete validation steps. The remaining gap for F071/F072 was locking that content with a focused docs contract test.
  • (2026-03-18) vitest runs this billing suite in randomized order, so invoiceGeneration.preview.test.ts needs to rebuild its default preview, PO, and selector-input mocks in beforeEach rather 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 billingCycleId plus the canonical execution window. Unlike unbridged contract-cadence rows, they do not need the full selectorInput payload 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 (from server/; passed after adding grouped-preview gating coverage for T097/T098)
  • (2026-03-20) pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false (from server/; passed after member-derived contract metadata warnings coverage for T099/T100)
  • (2026-03-20) pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false (from server/; passed after exhaustive cadence-source fallback coverage for T101)
  • (2026-03-20) pnpm exec vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled=false (from server/; passed after locking candidate-first target cardinality and candidate-page total semantics for T102/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" (from server/; passed)
  • (2026-03-20) pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts --coverage.enabled=false (from server/; passed after split-key grouping coverage for T106)
  • (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 (from server/; passed after removing legacy billing-window recurring generation shims for T107)
  • (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 (from server/; passed)
  • (2026-03-18) pnpm exec tsc --noEmit -p tsconfig.json (from server/; 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 (from server/; passed)
  • (2026-03-18) pnpm exec tsc --noEmit -p tsconfig.json (from packages/billing/; passed after fixing one implicit-any in billingAndTax.ts)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/recurringServicePeriodGenerationHorizon.domain.test.ts (from server/; 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 (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/integration/billingInvoiceTiming.integration.test.ts -t "T316/T323/T324/T327" (from server/; blocked in this environment because Postgres on 127.0.0.1:5438 refused connections)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts (from server/; 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 (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/automaticInvoices.recurringDueWork.ui.test.tsx --coverage.enabled=false (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/invoiceGeneration.preview.test.ts --coverage.enabled=false (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/contractPurchaseOrderSupport.ui.test.tsx --coverage.enabled=false (from server/; 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 (from server/; 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 (from server/; passed after tightening randomized-order mock resets in invoiceGeneration.preview.test.ts and 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 (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts --coverage.enabled=false (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/recurringBillingRunActions.test.ts --coverage.enabled=false (from server/; passed)
  • (2026-03-18) pnpm exec tsc --noEmit -p packages/billing/tsconfig.json (from repo root; passed after fixing one stale invoiceService.ts type assertion and the renamed AutomaticInvoices due-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" (from server/; 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 (from server/; 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 (from server/; 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" (from server/; 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" (from server/; passed)
  • (2026-03-18) pnpm exec vitest run ../packages/billing/tests/invoiceModification.recurringDeletionGuard.test.ts (from server/; 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" (from server/; passed)
  • (2026-03-18) pnpm exec vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts (from server/; passed after narrowing diagnostics to compatibility rows that survive merge without a persisted recordId)
  • (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 (from server/; 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" (from server/; 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 (from server/; passed)
  • (2026-03-18) pnpm exec tsc --noEmit -p server/tsconfig.json (from repo root; still fails on a pre-existing unrelated packages/billing/src/actions/creditActions.ts type 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 (from server/; 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 unrelated packages/billing/src/actions/creditActions.ts type 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 (from server/; 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 (from server/; 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" (from server/; 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 (from server/; 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" (from server/; passed after hardening persisted due-work row fetching/normalization and aligning the contract-cadence fixtures so they were truly unbridged)

Completed Checkpoints

  • (2026-03-20) Completed F079 and T107 by 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 F078 and T106 by 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 F077 and T104/T105 by blocking generation for partially materialized client-cadence windows at both due-work and invoice-generation layers: due-work candidates now flip canGenerate=false with 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 F076 and T102/T103 by preserving one-target-per-candidate mapping for client cadence candidates and switching selectClientCadenceRecurringRunTargets(...) to return paging totals from the due-work candidate response instead of recomputing totals from filtered target count.
  • (2026-03-20) Completed F075 and T101 by introducing exhaustive cadence-source badge formatting for AutomaticInvoices ready/history rows and rendering explicit unknown-state copy (Unknown cadence source (...)) instead of defaulting unexpected cadence-source values to Client schedule.
  • (2026-03-20) Completed F074 and T099/T100 by deriving contract and contract-line display from candidate members, preserving visible member-derived contract context even when metadata is partially missing, and surfacing explicit Contract metadata missing warning copy keyed to the affected candidate.
  • (2026-03-20) Completed F073 and T097/T098 by restricting AutomaticInvoices preview 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, and F007 by adding IRecurringDueWorkRow plus buildClientScheduleDueWorkRow(...) / 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 T001 through T005 with 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, and F013 by adding getAvailableRecurringDueWork(...) in billingAndTax.ts. The reader now pulls ready persisted rows from recurring_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, and T008 with unit coverage for billed/archived/superseded suppression and compatibility-row merge behavior when canonical persisted rows are absent.
  • (2026-03-18) Completed F014 / T013 by validating the pre-existing backfillRecurringServicePeriods(...) 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 / T014 by 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 / T015 by 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 T009 through T012 with an action-level due-work reader harness that exercises the real getAvailableRecurringDueWork(...) 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 T016 by 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, and T018 by widening the shared recurring charge-family contract and the recurring_service_periods migration constraint to include hourly and usage, 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, and T020 by linking persisted recurring_service_periods rows to the exact invoice_charge_details rows created during invoice persistence for fixed, product, and license recurring charges. The linkage now marks matching service periods as billed, stores invoice/item/detail IDs plus invoice_linked_at, and has focused DB-backed proof for both bridged client-cadence and unbridged contract-cadence invoice creation paths.
  • (2026-03-18) Completed F021 and T021 by repairing recurring service-period linkage during invoice hard-delete for linked recurring invoices. Deletion now clears invoice_id / charge linkage columns, restores affected rows to locked, 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 F022 by adding materializationGaps diagnostics to getAvailableRecurringDueWork(...). 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 F023 and T075 by adding RUNBOOK.md for 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 executable pnpm exec tsx helper invocations plus focused due-work reader verification.
  • (2026-03-18) Completed F024 and T023 by locking the unbridged contract-cadence duplicate-prevention identity onto buildRecurringRunSelectionIdentity(...). The selector-input path now has explicit proof that duplicate-prevention keys stay stable even when no billing_cycle_id bridge exists and the same execution window is rebuilt or deduplicated repeatedly.
  • (2026-03-18) Completed F025 through F032 and T025 through T031 by moving the AutomaticInvoices ready table onto getAvailableRecurringDueWork(...), swapping selection onto execution identities, rendering cadence/service-period/invoice-window/contract metadata, and generating mixed client-cadence plus contract-cadence batches through recurring-run targets. The UI now maps execution-identity failures back to the rendered contract row even when billing_cycle_id is 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, and T032/T033/T041/T042/T043 by introducing previewInvoiceForSelectionInput(...), wiring AutomaticInvoices preview selection to selectorInput, 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 F034 through F040, F042, and T034/T035/T036/T037/T038/T039/T040/T044/T045 by moving batch and preview-dialog generation fully onto selector-aware recurring-run targets, adding getPurchaseOrderOverageForSelectionInput(...), carrying execution identity through preview state and single-row PO confirmation, and surfacing an explicit No billing cycle bridge badge 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, and T048/T049/T050 by extracting a shared selector-input preview builder, routing legacy previewInvoice(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 keep executionIdentityKey plus nullable billingCycleId in those failures instead of collapsing diagnostics onto a fake bridge key.
  • (2026-03-18) Completed F044 and T046/T047 by 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 with billing_cycle_id = null, while the compatibility generateInvoice('cycle-1') wrapper still routes through the same selector-driven recurring selection path and creates the bridged legacy invoice row.
  • (2026-03-18) Completed F045 and T024 by extending duplicate prevention to unbridged contract-cadence selector inputs. The generation action now checks linked recurring_service_periods when no bridge billing_cycle_id exists and still returns the standard duplicate error contract with execution identity plus nullable bridge metadata.
  • (2026-03-18) Completed F049 through F056 and T022/T051/T052/T053/T054/T055/T056/T057/T058 by 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 requiring billing_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 F057 through F060, F063, F064, and T059/T060/T061/T062/T063/T064/T065 by extending the external API preview/generate request schemas to accept selector_input, routing InvoiceService onto the selector-aware billing action wrappers, and preserving compatibility billing_cycle_id callers through the same internal selector-input pipeline. The API layer no longer reimplements preview lookup against client_billing_cycles for unbridged contract-cadence requests.
  • (2026-03-18) Completed F061, F062, and T066/T067/T068 by extending invoice list/detail contracts with recurring execution metadata, allowing nullable billing_cycle_id in the API response schemas, and adding execution-window/cadence filters on top of a grouped recurring_service_periods summary join inside InvoiceService. 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 F065 through F068 and T076/T077/T078/T079/T080 by adding recurringServicePeriodActions.ts, a new Service Periods billing tab, and a tenant permission migration for billing.recurring_service_periods. Billing ops can now load one persisted schedule by scheduleKey, 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 F069 through F072 and T081/T082/T083/T084/T085 by locking recurring billing run workflow payloads to explicit contract-vs-mixed window identities, fixing AutomaticInvoices error-label fallback to use executionIdentityKey when 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 T069 through T073 with focused DB-backed metered recurring coverage in billingInvoiceTiming.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 T074 by adding focused unit coverage in billingEngine.test.ts for explicit service-driven metered timing. The billing engine now has locked proof that hourly and usage charge rows preserve canonical servicePeriodStart, servicePeriodEnd, and billingTiming metadata when calculated from selector-input service periods.
  • (2026-03-18) Completed T086 through T090 with DB-backed recurring happy-path coverage in billingInvoiceTiming.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, while billingAndTax.ts now 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 T091 through T096 with the last regression checkpoint. Compatibility client-cadence rows still preview and batch-generate through AutomaticInvoices, 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)

  • T108 candidate-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" (from server/; passed after making candidate discovery deterministic and avoiding over-strict canGenerate expectations)
  • Completed F080 and T108 by refactoring recurring due-work integration assertions to validate candidate-level contracts directly in billingInvoiceTiming.integration.test.ts without relying on invoiceCandidates.flatMap(...) row helpers for the new coverage path.
  • The legacy packages/clients/src/models/clientContractLine.ts path still queried client_contract_lines and could throw UUID filter errors when service_category was 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 (from server/; passed)
  • Completed F081 and T109 by migrating @alga-psa/clients admin billing configuration reads/updates off client_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 from client_contract_lines: it selected service_catalog.service_type and contract_line_service_bucket_config.total_hours, but live post-drop schema exposes contract_lines.contract_line_type and contract_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 (from server/; 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 (from server/; passed after updating the mocked post-drop join alias)
  • Completed F082 and T110 by migrating client-portal billing/account/services reads from client_contract_lines to 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(...) joined client_contracts by (template_contract_id OR contract_id), and bucketUsageService.calculatePeriod(...) joined contracts via coalesce(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:" (from server/; 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 (from server/; skipped by suite guards in this environment)
  • pnpm exec vitest run --coverage.enabled=false src/test/unit/bucketUsageService.test.ts (from server/; skipped by suite guards in this environment)
  • Completed F083 and T111 by removing template fallback joins from live billing computation paths (billingEngine.ts and bucketUsageService.ts) and adding DB-backed integration proof that billing-engine SQL uses canonical cc.contract_id = c.contract_id joins without template fallback expressions.
  • Contract assignment reads in contractActions.ts still accepted template IDs on instantiated query paths (getContractAssignments(...) and assignment counting in getContractSummary(...)) via orWhere 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 (from server/; passed)
  • Completed F084 and T112 by splitting instantiated assignment loading from template detail loading: assignment readers now resolve only by client_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 backfilled client_contracts.template_contract_id whenever 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 (from server/; passed)
  • Completed F085 and T113 by removing template_contract_id backfill writes from ContractLineService assignment flow and adding focused unit coverage that assignment cloning still works while proving no client_contracts mutation occurs when template provenance is absent.
  • pnpm exec vitest run --coverage.enabled=false src/test/unit/billing/clientContractLineRuntimeSourceGuards.static.test.ts (from server/; passed)
  • Completed F086 and T114 by extending the post-drop static runtime hygiene guard to scan packages/client-portal/src/actions and packages/clients runtime sources (actions + models) for forbidden client_contract_lines/client_contract_services table usage.
  • packages/billing/src/actions/billingAndTax.ts still owned local paginated due-work/materialization-gap interfaces; AutomaticInvoices.tsx imported the gap type from billing actions instead of the shared @alga-psa/types contract.
  • pnpm exec vitest run --coverage.enabled=false src/test/unit/billing/recurringDueWorkTypeContracts.static.test.ts (from server/; 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 (from server/; 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, and T116 by centralizing paginated recurring due-work and materialization-gap interfaces in @alga-psa/types, consuming those shared contracts in billing actions/UI, and explicitly tagging billingCycleId as 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_id bridge?