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

31 KiB

Scratchpad — Multi-Active Contracts Per Client

  • Plan slug: multi-active-contracts-per-client
  • Created: 2026-03-20

Decisions

  • 2026-03-20: This plan assumes true concurrent active assignments are allowed, including overlapping active windows for the same client.
  • 2026-03-20: This plan preserves single-assignment invoices. Removing the single-active-contract rule does not imply mixed-contract invoices.
  • 2026-03-20: PO scope remains invoice-level and therefore remains assignment-scoped because invoices remain assignment-scoped.
  • 2026-03-20: client_contract_id, not contract_id, is the canonical identity for assignment-scoped UI and execution.
  • 2026-03-20: Ambiguous legacy surfaces must stop guessing. Prefer explicit assignment identity or an explicit ambiguity failure.
  • 2026-03-20: Mixed-currency behavior is explicitly preserved as a separate policy; multi-active assignment support does not imply mixed-currency active assignments for the same client.
  • 2026-03-20: Invoice tables already snapshot client_contract_id, so removing singleton active-contract assumptions does not require invoice schema redesign to preserve single-assignment invoices.

Discoveries / Constraints

  • 2026-03-20: There is no DB-level uniqueness or exclusion constraint enforcing one active contract per client. The rule is app-layer only.
  • 2026-03-20: The billing wizard path is asymmetric today. It only preflights mixed-currency active contracts and can already create same-client same-currency active contracts through a different path than packages/clients.
  • 2026-03-20: packages/clients has the most dangerous identity bugs for this change. Several reads and UI flows still key by contract_id, which is not unique once a client can hold multiple active assignments to the same header/base contract.
  • 2026-03-20: Recurring due-work grouping is already closer to the desired behavior than preview/generation. Candidate grouping already splits by client_contract_id; the execution path is the risky part.
  • 2026-03-20: Invoice generation, PO consumption, invoice queries, and exports all still assume one invoice belongs to one client_contract_id. That assumption is workable and should be preserved in this plan.
  • 2026-03-20: Fixed recurring charge attribution can still collapse sibling concurrent assignments if they share the same base line/template identity.
  • 2026-03-20: Bucket usage currently picks the latest active matching assignment. That behavior becomes actively wrong when concurrent active contracts are allowed.
  • 2026-03-20: BillingCycles still collapses multiple active assignments for a client to the first active row returned, ordered by latest start date.
  • 2026-03-20: calculateBillingForSelectionInput(...) previously fetched persisted recurring timing selections at client + invoice window scope and passed them through unchanged, which could re-expand selected-candidate preview/generation into sibling assignment work.
  • 2026-03-20: Client-cadence selector scheduleKey encodes assignment line identity (client_contract_line:<id>), which can be used to enforce assignment-scoped recurring selection filtering before billing calculation.
  • 2026-03-20: Singleton active-contract UI/action blockers were still live in ContractBasicsStep, ContractDialog, ClientContractsTab, Contracts.tsx, contractActions, and shared/model helper layers.
  • 2026-03-20: Wizard assignment writes inserted directly into client_contracts, so shared assignment validation was bypassed and mixed-currency policy could diverge from clients flows.

Agent Audit Summary

  • UI enforcement audit:
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/ContractBasicsStep.tsx
    • packages/billing/src/components/billing-dashboard/contracts/ContractDialog.tsx
    • packages/billing/src/components/billing-dashboard/contracts/ClientContractsTab.tsx
    • packages/billing/src/components/billing-dashboard/contracts/Contracts.tsx
  • Shared/server invariant audit:
    • shared/billingClients/contracts.ts
    • shared/billingClients/clientContracts.ts
    • packages/billing/src/actions/contractActions.ts
    • packages/billing/src/models/contract.ts
  • Clients identity/scoping audit:
    • packages/clients/src/components/clients/ClientContractAssignment.tsx
    • packages/clients/src/components/clients/BillingConfiguration.tsx
    • packages/clients/src/components/clients/ContractLines.tsx
    • packages/clients/src/actions/clientContractLineActions.ts
    • packages/clients/src/models/clientContractLine.ts
  • Recurring/invoice/PO audit:
    • packages/billing/src/actions/invoiceGeneration.ts
    • packages/billing/src/lib/billing/billingEngine.ts
    • packages/billing/src/services/invoiceService.ts
    • packages/billing/src/services/purchaseOrderService.ts
    • packages/billing/src/actions/billingAndTax.ts
  • Secondary-surface audit:
    • packages/billing/src/components/billing-dashboard/BillingCycles.tsx
    • packages/billing/src/services/bucketUsageService.ts
    • packages/billing/src/actions/contractReportActions.ts
  • Fixture/schema/docs audit:
    • server/test-utils/billingTestHelpers.ts
    • server/test-utils/testContext.ts
    • docs/billing/billing.md
    • ee/docs/plans/2026-01-05-contract-purchase-order-support/*

Key File Pointers

  • Singleton helpers:
    • shared/billingClients/contracts.ts
    • shared/billingClients/clientContracts.ts
  • Billing UI blockers:
    • packages/billing/src/components/billing-dashboard/contracts/wizard-steps/ContractBasicsStep.tsx
    • packages/billing/src/components/billing-dashboard/contracts/ContractDialog.tsx
    • packages/billing/src/components/billing-dashboard/contracts/ClientContractsTab.tsx
  • Clients UI identity/scoping:
    • packages/clients/src/components/clients/ClientContractAssignment.tsx
    • packages/clients/src/actions/clientContractLineActions.ts
    • packages/clients/src/models/clientContractLine.ts
  • Invoice/PO boundary:
    • packages/billing/src/actions/invoiceGeneration.ts
    • packages/billing/src/services/purchaseOrderService.ts
    • packages/billing/src/actions/invoiceQueries.ts
  • Secondary ambiguity surfaces:
    • packages/billing/src/services/bucketUsageService.ts
    • packages/billing/src/components/billing-dashboard/BillingCycles.tsx

Commands / Runbooks

  • 2026-03-20: Singleton-rule repo scan
    • rg -n "hasActiveContractForClient|getClientIdsWithActiveContracts|already has an active contract|active contract overlapping|disabledClientIds" shared packages/billing packages/clients server/src/test ee/docs/plans docs/billing
  • 2026-03-20: Commit provenance for the UI client-disable behavior
    • git show --stat --summary 3aa57cd62f62ba70b2d05e657d6d1bc9d67b7b05
  • 2026-03-20: Plan scaffold
    • python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/scaffold_plan.py "Multi-Active Contracts Per Client" --slug multi-active-contracts-per-client
  • 2026-03-20: Singleton-blocker removal audit
    • rg -n "disabledClientIds|already has an active contract|terminate their current contract|save as draft|checkClientHasActiveContract|fetchClientIdsWithActiveContracts|getClientIdsWithActiveContracts|hasActiveContractForClient" packages/billing/src/components/billing-dashboard/contracts packages/billing/src/actions shared/billingClients packages/clients/src
  • 2026-03-20: Post-change verification scan
    • rg -n "checkClientHasActiveContract|fetchClientIdsWithActiveContracts|hasActiveContractForClient|getClientIdsWithActiveContracts" packages/billing/src packages/billing/tests shared/billingClients packages/billing/src/models
  • 2026-03-20: Shared overlap/mixed-currency follow-up scan
    • rg -n "join\\('client_contracts as cc'|already has an active contract overlapping the specified range|where\\(function overlap\\(" packages/billing/src/actions/contractWizardActions.ts shared/billingClients/clientContracts.ts packages/clients/src/actions/clientContractActions.ts packages/clients/src/models/clientContract.ts
  • 2026-03-20: Targeted billing test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/contract.test.ts tests/ClientContractsTab.assignmentLifecycle.test.ts tests/multiActiveContracts.singletonGuardRemoval.wiring.test.ts tests/multiActiveContracts.assignmentWritePath.wiring.test.ts
  • 2026-03-20: Clients identity wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.singletonGuardRemoval.wiring.test.ts tests/multiActiveContracts.assignmentWritePath.wiring.test.ts tests/multiActiveContracts.clientsAssignmentIdentity.wiring.test.ts tests/ClientContractsTab.assignmentLifecycle.test.ts tests/contract.test.ts
  • 2026-03-20: Client contract-line assignment read wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.singletonGuardRemoval.wiring.test.ts tests/multiActiveContracts.assignmentWritePath.wiring.test.ts tests/multiActiveContracts.clientsAssignmentIdentity.wiring.test.ts tests/multiActiveContracts.clientContractLineReads.wiring.test.ts tests/ClientContractsTab.assignmentLifecycle.test.ts tests/contract.test.ts
  • 2026-03-20: Client contract-line mutation-scope wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.singletonGuardRemoval.wiring.test.ts tests/multiActiveContracts.assignmentWritePath.wiring.test.ts tests/multiActiveContracts.clientsAssignmentIdentity.wiring.test.ts tests/multiActiveContracts.clientContractLineReads.wiring.test.ts tests/multiActiveContracts.clientContractLineMutationScope.wiring.test.ts tests/ClientContractsTab.assignmentLifecycle.test.ts tests/contract.test.ts
  • 2026-03-20: Assignment detail scope wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.assignmentDetails.wiring.test.ts tests/multiActiveContracts.clientsAssignmentIdentity.wiring.test.ts tests/multiActiveContracts.clientContractLineReads.wiring.test.ts tests/multiActiveContracts.clientContractLineMutationScope.wiring.test.ts
  • 2026-03-20: Disambiguation-copy fallback removal wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.disambiguationCopy.wiring.test.ts tests/multiActiveContracts.assignmentDetails.wiring.test.ts
  • 2026-03-20: Clients UI identity audit wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.clientsUiIdentityAudit.wiring.test.ts tests/multiActiveContracts.disambiguationCopy.wiring.test.ts tests/multiActiveContracts.assignmentDetails.wiring.test.ts
  • 2026-03-20: Recurring due-work assignment-split integration test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/recurringDueWorkReader.integration.test.ts
  • 2026-03-20: Recurring selector-scope preview/generation regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/invoiceGeneration.preview.test.ts src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts
  • 2026-03-20: Fixed recurring persistence assignment-separation regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/invoiceService.fixedPersistence.test.ts
  • 2026-03-20: Client-cadence assignment-scoped materialization-gap regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/recurringDueWorkReader.integration.test.ts
  • 2026-03-20: Mixed-assignment single-invoice invariant regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts
  • 2026-03-20: Recurring preview identity-context wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.recurringPreviewIdentity.wiring.test.ts
  • 2026-03-20: Invoice assignment-scoping wiring regression test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.invoiceAssignmentScoping.wiring.test.ts tests/ContractDetail.clientOwnedSemantics.wiring.test.ts tests/invoiceQueries.recurringDetailRefresh.wiring.test.ts
  • 2026-03-20: Invoice PO assignment-scope unit regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/invoiceQueries.purchaseOrderSummary.test.ts
  • 2026-03-20: BillingCycles multi-assignment summary wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.billingCyclesSummary.wiring.test.ts
  • 2026-03-20: Recurring PO-scope grouping wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.recurringPurchaseOrderScope.wiring.test.ts
  • 2026-03-20: Bucket usage ambiguity regression test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/bucketUsageService.periods.test.ts
  • 2026-03-20: Contract reports wording wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/ContractReports.summaryCopy.wiring.test.ts tests/contractReportActions.summary.wiring.test.ts tests/contractReportActions.expiration.wiring.test.ts
  • 2026-03-20: Reporting/export/accounting assignment-safety audit wiring test run
    • cd packages/billing && npx vitest run --config vitest.config.ts tests/multiActiveContracts.reportingExportAudit.wiring.test.ts
  • 2026-03-20: Billing test helper concurrent-assignment fixture wiring test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/billingTestHelpers.concurrentAssignments.wiring.test.ts
  • 2026-03-20: Direct concurrent-assignment seeding helper wiring test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/billingTestHelpers.concurrentAssignments.wiring.test.ts src/test/unit/billing/billingTestHelpers.directConcurrentSeed.wiring.test.ts
  • 2026-03-20: Legacy test assignment-identity regression wiring test run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/multiActiveContracts.legacyAssignmentTestAssumptions.wiring.test.ts src/test/unit/billing/billingTestHelpers.directConcurrentSeed.wiring.test.ts
  • 2026-03-20: Multi-active docs + singleton-regression static guard run
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/docs/multiActiveContracts.docsAndGuards.test.ts
  • 2026-03-20: T055 DB-backed integration attempt (environment blocked)
    • cd server && npx vitest run --config vitest.config.ts src/test/integration/billing/contractPurchaseOrderSupport.integration.test.ts --testNamePattern "T055"
    • Local run blocked by ECONNREFUSED to PostgreSQL on localhost:5438.
  • 2026-03-20: Remaining checklist static/wiring coverage run (T055/T056/T057/T058/T059/T060/T061/T065/T066/T067/T068/T069/T070/T072)
    • cd server && npx vitest run --config vitest.config.ts src/test/unit/billing/multiActiveContracts.remainingChecklist.wiring.test.ts

Implementation Log

  • 2026-03-20: Removed billing wizard/client-dialog active-contract singleton gating (client disable lists, active-contract warning copy, and submit-disable behavior tied to sibling active contracts).
  • 2026-03-20: Removed restore/set-active prechecks in both contract shells (ClientContractsTab and legacy Contracts.tsx) so activation no longer blocks on “another active contract exists”.
  • 2026-03-20: Removed action/model/shared singleton helper path:
    • deleted checkClientHasActiveContract(...) and fetchClientIdsWithActiveContracts(...) action exports
    • removed updateContract(... status: 'active') active-contract singleton rejection
    • removed shared/model hasActiveContractForClient(...) and getClientIdsWithActiveContracts(...) wrappers
    • removed expired-contract reactivation singleton precheck in shared contract reactivation helper
  • 2026-03-20: Updated contract-related billing tests/mocks to align with removed singleton helper exports and revised activation callback signatures.
  • 2026-03-20: Routed wizard assignment persistence through shared createClientContractAssignment(...) and removed wizard-local mixed-currency preflight query so create semantics come from shared assignment writes.
  • 2026-03-20: Removed shared assignment overlap-window create/update blockers while preserving the clients action-layer invoiced-period guard.
  • 2026-03-20: Centralized packages/clients assignment create/update persistence through shared helpers (createClientContractAssignment / updateClientContractAssignment) and removed duplicate overlap enforcement in clients actions/models.
  • 2026-03-20: Updated ClientContractAssignment to keep assignment flows keyed by client_contract_id:
    • add/apply now uses the returned assignment id from assignContractToClient(...)
    • removed contract-header de-dup filtering that blocked creating a second active assignment for the same contract_id
  • 2026-03-20: Refactored clients contract-line reads to emit assignment-scoped synthetic identity (contract-<client_contract_id>-<contract_line_id>) instead of aliasing raw contract_line_id as client_contract_line_id.
  • 2026-03-20: Added synthetic identity parsing in clients contract-line action/model paths so historical invoice guards and mutations can resolve back to underlying contract-line IDs without reintroducing contract-header-only read identity.
  • 2026-03-20: Made contract-line mutation scope explicit by requiring assignment-scoped synthetic line identity and failing with an explicit ambiguity error when multiple active assignments share the same contract header.
  • 2026-03-20: Added explicit assignment selection context to the clients Contract Lines UI and plumbed selected client_contract_id into add-line payloads.
  • 2026-03-20: Wired ClientContractAssignment mutation callbacks into BillingConfiguration (onAssignmentsChanged) so assignment create/edit/deactivate refreshes line/overlap data immediately instead of leaving stale assignment-scoped views.
  • 2026-03-20: Scoped client assignment detail metadata to the selected assignment in ClientContract.getDetailedClientContract(...):
    • contract line names/count now come from active client_contract_lines rows filtered by client_contract_id
    • removed contract-header-wide contract_lines lookup that collapsed sibling active assignments sharing a contract_id
  • 2026-03-20: Added T030 static wiring coverage (multiActiveContracts.assignmentDetails.wiring.test.ts) asserting assignment-detail data is sourced by client_contract_id and edit dialog consumes selected-assignment line names.
  • 2026-03-20: Removed disambiguation guide wording that implied hidden fallback selection (for example “most recently created contract line” and implicit system default picks) and replaced with explicit ambiguity-error/user-choice guidance.
  • 2026-03-20: Added T031 static wiring coverage (multiActiveContracts.disambiguationCopy.wiring.test.ts) to prevent fallback copy regressions.
  • 2026-03-20: Audited targeted packages/clients UI identity surfaces and removed remaining ambiguous assignment picker labeling in ContractLines by including explicit assignment identity (client_contract_id prefix) in option labels.
  • 2026-03-20: Added T032 focused wiring coverage (multiActiveContracts.clientsUiIdentityAudit.wiring.test.ts) asserting assignment picker/state/overlap matrix flows remain keyed to assignment-scoped identities instead of contract_id uniqueness assumptions.
  • 2026-03-20: Added T033 regression coverage in server/src/test/unit/billing/recurringDueWorkReader.integration.test.ts proving same-client/same-window recurring rows with different client_contract_id values remain split into separate invoice candidates (with split reasons reflecting single-contract and PO-scope boundaries).
  • 2026-03-20: Scoped recurring preview/generation billing selection execution to the selected selector identity in calculateBillingForSelectionInput(...):
    • client-cadence selection now filters persisted recurring timing selections to the selected client_contract_line parsed from selector scheduleKey
    • contract-cadence selection now filters persisted recurring timing selections to the selected contractLineId
    • both paths now fail explicitly when selector-scoped rows are missing instead of silently re-expanding to sibling due work
  • 2026-03-20: Added selector-scope regression coverage:
    • T034/T036 in server/src/test/unit/billing/invoiceGeneration.preview.test.ts
    • T035/T037 in server/src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts
    • Expanded both test harness query builders to support whereIn and overloaded where(...) signatures used by selector normalization paths.
  • 2026-03-20: Fixed fixed-recurring consolidated parent grouping in persistFixedInvoiceCharges(...) so grouping identity is client_contract_id + client_contract_line_id (assignment-scoped), not line-id-only.
  • 2026-03-20: Updated consolidated-plan metadata lookup to query by source line identity while retaining assignment-scoped grouping keys.
  • 2026-03-20: Added T038 regression in server/src/test/unit/billing/invoiceService.fixedPersistence.test.ts proving sibling assignments sharing one base contract line persist as separate parent invoice charges with distinct client_contract_id attribution.
  • 2026-03-20: Refined client-cadence materialization-gap candidate blocking to match assignment-scoped recurring identities instead of broad client + invoice window keys.
  • 2026-03-20: Added T039 regression in server/src/test/unit/billing/recurringDueWorkReader.integration.test.ts proving a materialization gap on one assignment does not block sibling assignment candidates in the same client invoice window.
  • 2026-03-20: Preserved explicit single-assignment invoice failure behavior by attaching recurring execution-window context when selector-input generation encounters mixed client_contract_id charge sets.
  • 2026-03-20: Added T040 regression in server/src/test/unit/billing/invoiceGeneration.selectorInputGenerate.test.ts proving selector-input generation fails explicitly (with user-readable single-assignment invariant message) when billing charges span multiple assignments.
  • 2026-03-20: Enhanced recurring ready-to-invoice contract rendering with explicit assignment-context labels (assignment line/schedule identity fallback) so same-named concurrent contract candidates remain visually distinct.
  • 2026-03-20: Added T041 wiring coverage in packages/billing/tests/multiActiveContracts.recurringPreviewIdentity.wiring.test.ts asserting assignment-context rendering tokens are present in AutomaticInvoices.
  • 2026-03-20: Corrected fetchInvoicesByContract(...) assignment scoping by filtering invoice reads with invoices.client_contract_id instead of client_contracts.contract_id (header identity), preventing sibling-assignment invoice leakage when one contract header has multiple active assignments.
  • 2026-03-20: Updated ContractDetail invoice-tab loading to scope invoice reads by selected clientContractId (fallback first assignment) and clarified assignment-scoped error copy.
  • 2026-03-20: Added T042 wiring coverage in packages/billing/tests/multiActiveContracts.invoiceAssignmentScoping.wiring.test.ts asserting invoice query and contract-detail invoice tab use assignment (client_contract_id) identity.
  • 2026-03-20: Added T043 unit coverage in server/src/test/unit/billing/invoiceQueries.purchaseOrderSummary.test.ts proving PO context/consumption lookups are keyed by invoice.client_contract_id and do not drift to sibling active assignments.
  • 2026-03-20: Updated BillingCycles assignment summary mapping to retain and render all active client_contract_id rows per client (with assignment identity labels) instead of collapsing to the first contract row.
  • 2026-03-20: Added T045 wiring coverage in packages/billing/tests/multiActiveContracts.billingCyclesSummary.wiring.test.ts asserting multi-assignment summary rendering path remains active.
  • 2026-03-20: Added T044 wiring coverage in packages/billing/tests/multiActiveContracts.recurringPurchaseOrderScope.wiring.test.ts asserting recurring candidate grouping retains purchaseOrderScopeKey = client_contract_id.
  • 2026-03-20: Replaced implicit bucket assignment fallback in calculatePeriod(...) by detecting conflicting active assignment matches and throwing an explicit ambiguity error instead of silently choosing one.
  • 2026-03-20: Added T046 regression in server/src/test/unit/billing/bucketUsageService.periods.test.ts proving overlapping eligible assignments fail explicitly with actionable assignment context.
  • 2026-03-20: Updated Contract Reports summary wording so assignment-based counts are labeled as Active assignments instead of Billable clients.
  • 2026-03-20: Added T047 wiring coverage in packages/billing/tests/ContractReports.summaryCopy.wiring.test.ts to prevent report-label regressions that conflate assignment counts with client counts.
  • 2026-03-20: Completed reporting/export/accounting audit with focused assignment-safety checks:
    • contractReportActions.ts summary keeps assignment-grain counting (countDistinct client_contract_id) for renewal-decision totals.
    • invoiceQueries.ts PO summary reads invoice header assignment (invoice.client_contract_id) and resolves PO context/consumption by that assignment only.
    • purchaseOrderService.ts consumed/authorized PO lookups are scoped to client_contract_id (not client-level active-contract selection).
    • accountingExportService.ts contains no active-contract singleton selector/helper usage; export execution remains invoice-line authoritative.
  • 2026-03-20: Added T048 audit wiring coverage in packages/billing/tests/multiActiveContracts.reportingExportAudit.wiring.test.ts to lock audited assignment-safe semantics and fail if singleton selector helpers reappear in targeted files.
  • 2026-03-20: Expanded createFixedPlanAssignment(...) fixture controls for multi-active scenarios with explicit lifecycle knobs:
    • contract header lifecycle (contractHeaderIsActive, contractHeaderStatus)
    • assignment lifecycle (assignmentIsActive, assignmentStatus)
    • assignment PO fields (assignmentPoRequired, assignmentPoNumber, assignmentPoAmount)
    • assignment-line active toggle (clientContractLineIsActive)
  • 2026-03-20: Added createConcurrentFixedPlanAssignments(...) helper to seed two or more intentionally concurrent assignment fixtures via one call.
  • 2026-03-20: Added T049 wiring coverage in server/src/test/unit/billing/billingTestHelpers.concurrentAssignments.wiring.test.ts asserting lifecycle knobs and concurrent-assignment helper are present and wired.
  • 2026-03-20: Added seedConcurrentClientContractAssignmentsDirect(...) helper for explicit direct DB fixture seeding through TestContext.createEntity(...):
    • requires contracts + client_contracts tables
    • supports per-assignment header/assignment lifecycle overrides
    • intentionally bypasses production write paths for tests that need concurrent states directly
  • 2026-03-20: Added T050 wiring coverage in server/src/test/unit/billing/billingTestHelpers.directConcurrentSeed.wiring.test.ts asserting direct concurrent seeding uses createEntity(...) on contracts and client_contracts.
  • 2026-03-20: Updated legacy integration assertions that previously depended on client_id + first() / latest-row behavior:
    • contractWizard.integration.test.ts now reads client assignment using explicit contract_id: result.contract_id
    • contractPurchaseOrderSupport.integration.test.ts now captures wizard result and reads assignment by explicit contract_id: wizardResult.contract_id (removed orderBy(created_at desc) fallback)
  • 2026-03-20: Added T051 wiring coverage in server/src/test/unit/billing/multiActiveContracts.legacyAssignmentTestAssumptions.wiring.test.ts to prevent reintroduction of latest-assignment test assumptions in targeted integration suites.
  • 2026-03-20: Updated product docs/runbooks to remove singleton-active assumptions and preserve explicit invoice boundary:
    • docs/billing/billing.md assignment lifecycle section now states concurrent active assignments are allowed (with mixed-currency still blocked as a separate rule)
    • ee/docs/plans/2026-01-05-contract-purchase-order-support/PRD.md now describes invoices.client_contract_id as single-assignment invoice scope (not single-active prerequisite)
    • ee/docs/plans/2026-01-05-contract-purchase-order-support/SCRATCHPAD.md now states one PO per invoice without assuming one active contract per client
  • 2026-03-20: Added consolidated static/docs guard coverage in server/src/test/unit/docs/multiActiveContracts.docsAndGuards.test.ts:
    • T052: singleton UI/action guard patterns stay removed
    • T053: billing docs no longer claim active-assignment overlap blocking
    • T054: contract PO plan docs no longer depend on single-active-client prerequisite
    • T062: mixed-currency guard remains explicit and separate from removed singleton helpers
    • T063/T064: repo-wide static guard for removed singleton helper usage
    • T071: runbook notes explicitly record invoice.client_contract_id snapshot boundary and no schema redesign requirement
  • 2026-03-20: Added T055 DB-backed integration scenario in contractPurchaseOrderSupport.integration.test.ts:
    • first active assignment via wizard (createClientContractFromWizard)
    • second active assignment via quick-add/action path (createContract + assignContractToClient)
    • asserts same-client active assignments include both contract paths
  • 2026-03-20: Added consolidated remaining-checklist guard coverage in server/src/test/unit/billing/multiActiveContracts.remainingChecklist.wiring.test.ts:
    • T056/T057/T058/T059: clients assignment rendering, line add/edit/remove scope, and overlap identity remain assignment-scoped
    • T060/T061: recurring due-work selection and invoice/history reads remain assignment-scoped (client_contract_id)
    • T065: BillingCycles continues rendering multiple active assignments per client
    • T066: bucket ambiguity errors include conflicting assignment context
    • T067: recurring preview displays explicit assignment context for same-named contracts
    • T068: wizard and clients flows both route through shared assignment-create semantics
    • T069: header activation/reactivation paths remain free of sibling-active singleton blockers
    • T070: mixed-currency behavior remains explicitly covered and separate from singleton removal
    • T072: reporting/export/accounting assignment-safe wording and audit coverage remain in place
  • Related plans:
    • ee/docs/plans/2026-03-18-service-driven-invoicing-cutover/
    • ee/docs/plans/2026-03-20-template-runtime-normalization-completion/
    • ee/docs/plans/2026-01-05-contract-purchase-order-support/
  • Product docs:
    • docs/billing/billing.md

Open Questions

  • Should the mixed-currency rule remain a separate restriction, or should this plan remove it too?
  • For bucket usage ambiguity, should product UX require explicit assignment identity upstream or accept a hard failure at billing time?
  • Are there any remaining client-facing surfaces where “contract” is actually intended to mean assignment and should be renamed in this plan?