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

265 lines
66 KiB
Markdown

# 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)
## Links / References
- Broad architecture plan:
- [PRD.md](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/ee/docs/plans/2026-03-16-service-period-first-billing-and-cadence-ownership/PRD.md)
- Key files:
- [AutomaticInvoices.tsx](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx)
- [billingAndTax.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/actions/billingAndTax.ts)
- [billingCycleActions.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/actions/billingCycleActions.ts)
- [invoiceGeneration.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/actions/invoiceGeneration.ts)
- [billingEngine.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/lib/billing/billingEngine.ts)
- [invoiceSchemas.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/lib/api/schemas/invoiceSchemas.ts)
- [InvoiceService.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/lib/api/services/InvoiceService.ts)
- [materializeClientCadenceServicePeriods.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/shared/billingClients/materializeClientCadenceServicePeriods.ts)
- [materializeContractCadenceServicePeriods.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/shared/billingClients/materializeContractCadenceServicePeriods.ts)
- [recurringDueWork.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/shared/billingClients/recurringDueWork.ts)
- [recurringTiming.interfaces.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/types/src/interfaces/recurringTiming.interfaces.ts)
- [recurringDueWork.domain.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringDueWork.domain.test.ts)
- [billingAndTax.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/actions/billingAndTax.ts)
- [recurringServicePeriodDueSelection.domain.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringServicePeriodDueSelection.domain.test.ts)
- [backfillRecurringServicePeriods.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/shared/billingClients/backfillRecurringServicePeriods.ts)
- [recurringServicePeriodBackfill.domain.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringServicePeriodBackfill.domain.test.ts)
- [recurringServicePeriodActions.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/actions/recurringServicePeriodActions.ts)
- [RecurringServicePeriodsTab.tsx](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/packages/billing/src/components/billing-dashboard/RecurringServicePeriodsTab.tsx)
- [20260318194500_add_recurring_service_period_permissions.cjs](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/migrations/20260318194500_add_recurring_service_period_permissions.cjs)
- [recurringServicePeriodActions.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringServicePeriodActions.test.ts)
- [recurringServicePeriodsTab.ui.test.tsx](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringServicePeriodsTab.ui.test.tsx)
- [recurringBillingRunWindowIdentity.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/billing/recurringBillingRunWindowIdentity.test.ts)
- [serviceDrivenInvoicingCutover.runbook.test.ts](/Users/roberisaacs/alga-psa.worktrees/feature/client-owned-contracts-simplification/server/src/test/unit/docs/serviceDrivenInvoicingCutover.runbook.test.ts)
## 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?