Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
28 KiB
Pass 0 Appendix — Inventory, Parity, and Compatibility Boundaries
This appendix turns the PRD's pass-0 goals into implementation-grade artifacts. It is intentionally source-backed: the file inventory below is paired with pass-0-source-inventory.json, and a docs contract test keeps the appendix aligned with live code references.
Architecture Flow
Recurring billing normalization in this plan follows one authoritative sequence:
- cadence owner determines the boundary generator
- the boundary generator emits canonical service periods
- assignment activity windows intersect those service periods
- due-position rules map service periods onto invoice windows
- invoice selection chooses due recurring work for a billing run
- persisted invoice detail rows carry the canonical service-period metadata downstream
Anything that still derives timing from invoice headers, billing_cycle_alignment, or charge-family-specific proration is inventory debt or cleanup work.
System-Surface Matrix
| Surface | Key files / seams | Why it is in scope | Pass-0 requirement |
|---|---|---|---|
| Runtime billing | packages/billing/src/lib/billing/billingEngine.ts, shared/billingClients/createBillingCycles.ts, packages/billing/src/lib/billing/createBillingCycles.ts |
Current runtime still mixes client cycles, assignment dates, billing_timing, enable_proration, and billing_cycle_alignment. |
Inventory live timing controls and define the parity target before refactors begin. |
| Invoice generation and persistence | packages/billing/src/actions/invoiceGeneration.ts, packages/billing/src/services/invoiceService.ts |
Recurring timing leaks into invoice detail persistence, duplicate prevention, and billed-through behavior. | Document the header-vs-detail timing split and the future cut line. |
| Credits / prepayment / negative invoices | packages/billing/src/actions/creditActions.ts, packages/billing/src/actions/invoiceModification.ts, server/src/test/infrastructure/billing/credits/*, server/src/test/infrastructure/billing/invoices/prepaymentInvoice.test.ts, server/src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts |
These flows already depend on recurring timing metadata and are likely to regress if service periods move. | Capture the existing consumers before any timing model changes. |
| Pricing / discounts / PO / tax | server/src/test/infrastructure/billing/pricingSchedules/pricingScheduleRateOverrides.test.ts, packages/billing/src/services/purchaseOrderService.ts, packages/billing/src/services/accountingExportInvoiceSelector.ts |
These features evaluate dates or grouping constraints that depend on recurring timing semantics. | Add them to the parity matrix instead of treating them as follow-on surprises. |
| Data model / API / repositories | server/src/lib/repositories/contractLineRepository.ts, server/src/lib/api/services/ContractLineService.ts, server/src/lib/api/schemas/contractLineSchemas.ts, packages/billing/src/repositories/contractLineRepository.ts |
billing_cycle_alignment, billing_timing, and enable_proration are still propagated by write paths and read models. |
Inventory live storage and propagation points before introducing cadence_owner. |
| UI authoring and settings | packages/billing/src/components/billing-dashboard/ContractLineDialog.tsx, packages/billing/src/components/billing-dashboard/contracts/ContractWizard.tsx, packages/billing/src/components/billing-dashboard/contracts/QuickStartGuide.tsx, packages/billing/src/actions/billingCycleAnchorActions.ts |
Current UI teaches proration-centric workarounds and assumes client cadence is universal. | Capture every authoring surface that must be updated with explicit cadence language. |
| Portal / reporting / downstream readers | packages/client-portal/src/actions/account.ts, packages/client-portal/src/actions/client-portal-actions/client-billing.ts, packages/billing/src/actions/contractReportActions.ts |
These readers mostly flatten contract or invoice-header timing today and will need an explicit post-cutover policy. | Document them now so parity is checked at the reader contract, not only in the engine. |
| Accounting exports | packages/billing/src/services/accountingExportInvoiceSelector.ts, packages/billing/src/repositories/accountingExportRepository.ts, packages/billing/src/adapters/accounting/xeroAdapter.ts, packages/billing/src/adapters/accounting/quickBooksOnlineAdapter.ts |
Export adapters already persist or flatten service-period fields and are sensitive to shape drift. | Define their current data contract and include them in parity checks. |
| Migration / cleanup | server/migrations/20251025120000_add_billing_timing_metadata.cjs, server/migrations/20251028120000_consolidate_contract_line_rates.cjs |
Historical timing fields and compatibility migrations already exist. | Treat migration state as part of the live system, not a background assumption. |
Live Timing-Control Inventory
Source-backed file lists live in pass-0-source-inventory.json.
Current controls that materially affect recurring timing
| Control | Current role | Evidence anchors |
|---|---|---|
resolveServicePeriod |
Billing-engine helper that still decides charge timing directly for recurring execution. | packages/billing/src/lib/billing/billingEngine.ts, server/src/test/unit/billing/billingEngine.timing.test.ts |
billing_cycle_alignment |
Legacy timing selector persisted across migrations, models, APIs, actions, and UI. | See billingCycleAlignmentRefs in pass-0-source-inventory.json. |
billing_timing |
Advance-vs-arrears switch that currently changes which period is billed and how detail metadata is written. | packages/billing/src/lib/billing/billingEngine.ts, packages/billing/src/services/invoiceService.ts, server/src/test/integration/billingInvoiceTiming.integration.test.ts, contract line schemas and UI actions. |
enable_proration |
Current user-facing workaround for partial-period coverage; still propagated through write paths and tests. | packages/billing/src/lib/billing/billingEngine.ts, server/src/lib/api/services/ContractLineService.ts, contract wizard/dialog components, infrastructure billing tests. |
| Client billing frequency and generated client cycles | Current recurring clock for existing tenants and the main parity baseline. | shared/billingClients/createBillingCycles.ts, packages/billing/src/lib/billing/createBillingCycles.ts, packages/billing/src/actions/invoiceGeneration.ts, packages/billing/src/actions/billingCycleActions.ts. |
| Client anchor settings | Monthly / quarterly / semi-annual / annual anchor behavior that must be preserved under client cadence. | shared/billingClients/billingCycleAnchors.ts, packages/billing/src/lib/billing/billingCycleAnchors.ts, packages/billing/src/actions/billingCycleAnchorActions.ts, server/src/test/infrastructure/billing/invoices/clientBillingCycleAnchors.test.ts. |
| Assignment start / end dates | Current overlay used to trim or skip recurring work and to explain portal contract periods. | shared/billingClients/contractLines.ts, packages/client-portal/src/actions/account.ts, packages/billing/src/lib/billing/billingEngine.ts. |
Recurring product migration seam inventory
BillingEngine.calculateBilling(...)no longer applies a late-stage product-onlyapplyProrationToPlan(...)branch; product coverage settlement now happens insidecalculateProductCharges(...)after canonical due-period selection.calculateProductCharges(...)now resolves due product periods through the shared recurring timing helper, soservicePeriodStartandservicePeriodEndcome from canonical service-period selection rather than the enclosing invoice window.- Product selection now excludes license-tagged catalog rows so product and license recurring families do not double-bill the same catalog item once the dedicated license path is live.
- Product tax now evaluates against the canonical due service-period end date, which keeps service-date semantics attached to the recurring work being billed instead of the enclosing invoice window.
Recurring license migration seam inventory
BillingEngine.calculateBilling(...)no longer defers license coverage settlement to the generic late-stageapplyProrationToPlan(...)branch; license coverage settlement now happens inside the shared recurring quantity-charge helper after canonical due-period selection.calculateLicenseCharges(...)now resolves due license periods through the same shared recurring timing helper used by fixed and product recurring charges, soperiod_start,period_end,servicePeriodStart, andservicePeriodEndfollow the canonical due service period.- License selection now uses explicit
service_catalog.item_kind = productplusis_license = trueselection so the placeholder license query is gone, without changing the catalog/storage surface used by contract-line authoring. - License tax and period metadata now evaluate from the canonical due service period, which keeps tax-date semantics aligned with the billed license period rather than the enclosing invoice window.
Persisted Date and Period Fields
These are the persisted fields that currently participate in recurring timing or billed-through semantics.
| Persisted field(s) | Current table / surface | Current purpose | Evidence |
|---|---|---|---|
client_billing_cycles.start_date, client_billing_cycles.end_date, billing_cycle_id |
Client billing cycle storage and generation actions | Primary recurring invoice window for current client-cadence execution. | shared/billingClients/createBillingCycles.ts, packages/billing/src/actions/invoiceGeneration.ts, packages/billing/src/actions/billingCycleActions.ts |
invoices.billing_period_start, invoices.billing_period_end |
Invoice headers | Current grouping metadata; some readers still derive recurring meaning from it. | packages/billing/src/actions/invoiceGeneration.ts, server/src/lib/api/services/InvoiceService.ts, packages/billing/src/services/accountingExportInvoiceSelector.ts |
invoice_item_details.service_period_start, invoice_item_details.service_period_end, invoice_item_details.billing_timing |
Invoice detail persistence | Current detail-level recurring timing record for advance/arrears and export consumers. | packages/billing/src/services/invoiceService.ts, server/migrations/20251025120000_add_billing_timing_metadata.cjs, server/src/test/integration/billingInvoiceTiming.integration.test.ts |
client_contract_lines.start_date, client_contract_lines.end_date |
Assignment / recurring obligation activity window | Current overlay for partial-period starts, terminations, and portal display. | shared/billingClients/contractLines.ts, packages/client-portal/src/actions/account.ts, packages/billing/src/lib/billing/billingEngine.ts |
Accounting export batch line service_period_start, service_period_end |
Export read model | Flattened downstream representation used by adapters and export history readers. | packages/billing/src/repositories/accountingExportRepository.ts, packages/billing/src/adapters/accounting/xeroAdapter.ts, packages/billing/src/adapters/accounting/quickBooksOnlineAdapter.ts |
Service-Period Consumer Inventory
Detail-level consumers already reading canonical-like period fields
- Invoice detail persistence and hydration:
packages/billing/src/services/invoiceService.tsserver/src/interfaces/billing.interfaces.ts
- Accounting exports:
packages/billing/src/services/accountingExportInvoiceSelector.tspackages/billing/src/repositories/accountingExportRepository.tspackages/billing/src/adapters/accounting/xeroAdapter.tspackages/billing/src/adapters/accounting/quickBooksOnlineAdapter.ts
- Accounting integration tests:
server/src/test/integration/accounting/invoiceSelection.integration.test.tsserver/src/test/integration/accounting/batchLifecycle.integration.test.tsserver/src/test/integration/accounting/xeroLiveExport.integration.test.ts
Dependent flows that already rely on recurring timing, even if they do not always read detail fields directly
- Credits and reconciliation:
packages/billing/src/actions/creditActions.tspackages/billing/src/actions/creditReconciliationActions.tsserver/src/test/infrastructure/billing/credits/*
- Prepayment and negative invoices:
packages/billing/src/actions/invoiceModification.tspackages/billing/src/actions/manualInvoiceActions.tsserver/src/test/infrastructure/billing/invoices/prepaymentInvoice.test.tsserver/src/test/infrastructure/billing/invoices/negativeInvoiceCredit.test.ts
Downstream readers that still flatten or infer timing
- Client portal:
packages/client-portal/src/actions/account.tspackages/client-portal/src/actions/client-portal-actions/client-billing.tspackages/client-portal/src/components/account/BillingSection.tsx- These currently derive cycle and contract periods from client billing cycles or assignment dates, not authoritative recurring detail rows.
- Reporting:
packages/billing/src/actions/contractReportActions.ts- These reports currently summarize invoice dates, contract dates, or assignment dates. They are downstream consumers because service-period-first billing can change which date basis is authoritative.
Compatibility Boundaries for Explicitly Out-of-Scope Flows
| Flow | Scope status in this PRD | Compatibility boundary during v1 recurring work |
|---|---|---|
| Time entry billing | Out of first hard cut | May continue using event-driven usage dates and existing billed-through assumptions; recurring refactors must not silently change it. |
| Usage-record billing | Out of first hard cut | Usage event windows remain their own truth source until a separate follow-on plan explicitly unifies them. |
| Materials / non-recurring charges | Out of first hard cut | Manual or one-off financial artifacts stay intentionally outside the canonical recurring schedule. |
| Manual-only invoice flows | Out of first hard cut for canonical schedule generation | Manual invoices must coexist with recurring detail periods but must not begin generating service periods on their own during v1. |
| Bucket usage metrics outside recurring bucket contract lines | Explicit follow-on / partial scope only | Bucket readers that rely on usage period tables stay on current semantics unless the line is part of recurring contract-backed billing in scope for this plan. |
Explicit v1 exclusions by behavior
- Time entry billing stays event-driven in v1:
- selection continues to use
time_entries.start_time/time_entries.end_timeagainst the invoice window - no persisted recurring service-period ledger is generated for time entries during this cut
- billed-through, duplicate prevention, and credit behavior for time entry billing must remain on the current invoice-window/date-query model until a separate follow-on plan says otherwise
- selection continues to use
- Usage-record billing stays event-driven in v1:
- selection continues to use usage-event dates and current end-exclusive overlap rules
- no canonical recurring service periods are generated for ad hoc usage records during this cut
- recurring service-period-first work must not silently change how usage events are grouped, billed-through, or credited outside explicit recurring bucket overlays already in scope
- Bucket behavior is split explicitly:
- in scope now: recurring bucket contract lines where allowance periods, rollover, overage charging, and tax-date evaluation already depend on recurring timing semantics
- still out of scope: generic bucket reporting, remaining-unit readers, and other bucket metrics that are not tied to recurring contract-backed billing selection
- bucket usage period tables remain the source of truth for those out-of-scope readers until a follow-on plan deliberately unifies them
Follow-On Boundary — Full Time And Usage Unification
Recurring v1 does not silently expand into a general time-and-usage service-period ledger.
The explicit follow-on trigger is:
- recurring materialized service periods must already exist as stable future billing objects
- regeneration, override preservation, locking, and invoice linkage must already be proven on recurring contract-backed charges
- operator and finance workflows must already be able to explain canonical recurring periods without relying on invoice headers as the real timing source
Until that trigger is met:
- time-entry billing keeps
time_entries.start_time/time_entries.end_timeand current invoice-window overlap semantics as its authoritative selection model - usage-record billing keeps usage-event timestamps and current billed-through semantics as its authoritative selection model
- credits, exports, and reconciliation continue to treat time and usage as event-driven financial artifacts instead of inferred service-period-ledger rows
The future follow-on plan must define, not assume:
- whether time and usage adopt their own persisted period ledger, a projection onto the recurring ledger model, or a different canonical period abstraction entirely
- how event timestamps map onto any canonical period without losing source-event truth
- how mixed event-driven and period-driven invoice readers, exports, and analytics stay explainable during coexistence
- what migration and rollback posture applies once time or usage stop being purely event-driven
Follow-On Boundary — Advanced Service-Period Ledger Extensions
Recurring v1 must stop at the first authoritative persisted service-period ledger that supports:
- generation within the v1 horizon
- explicit future-period edits
- regeneration with override preservation
- invoice linkage and billed-period locking
The following are not implicit v1 requirements and must stay follow-on work unless the first-cut ledger proves insufficient:
- long-range materialization horizons intended for years of future schedule projection
- archival or cold-storage pipelines for billed, superseded, or otherwise inactive service-period records
- performance-oriented denormalization such as ledger summary tables, read-side caches, or bulk projection views
- mass-repair or bulk backfill tooling aimed at reshaping the ledger beyond the rollout-safe correction flows defined for v1
Trigger this follow-on only when there is source-backed evidence that v1 cannot stay operationally safe without it, for example:
- regeneration or due-selection cost that cannot be contained within the v1 horizon policy
- support, dashboard, or export reads that remain too slow when pointed at the canonical ledger shape
- storage or retention requirements that force archival semantics beyond the initial billed-history retention model
When that follow-on starts, it must define:
- which record remains the canonical source of truth versus which projections are derivative
- how archival, compaction, or denormalization preserves invoice linkage, provenance, and auditability
- replay and repair behavior when derived ledger structures drift from canonical persisted periods
- rollback posture if tenants temporarily carry both the canonical ledger and performance-oriented derivatives
Follow-On Boundary — Persisted Recurring Execution Records
Recurring v1 includes typed execution-window identity, selector inputs, and retry keys. It does not automatically include a durable recurring-run ledger or persisted due-selection snapshots.
Keep the following out of v1 unless there is concrete operational evidence they are needed:
- persisted recurring run records keyed by execution-window identity
- durable selection snapshots for every due-work batch
- operator-facing replay history beyond current job metadata, audit logs, and canonical invoice/detail persistence
- repair tooling that depends on replaying a stored execution ledger instead of recomputing due work from current source truth
Trigger this follow-on only when:
- support or finance workflows cannot explain failed or retried recurring runs from current scheduler metadata
- contract-cadence execution creates ambiguity that transient logs cannot resolve safely
- replay/debug requirements become strong enough that transient execution identity is no longer sufficient
When that follow-on starts, it must define:
- the authoritative relationship between persisted execution records and canonical recurring service-period truth
- retention, replay, and repair rules for execution records that outlive transient jobs
- rollback posture when some tenants have durable recurring execution records and others still rely on transient scheduler metadata
Follow-On Boundary — Invoice-Schema Versioning
Recurring v1 keeps old-shape and new-shape invoice support additive. It does not automatically introduce a formal invoice-schema version boundary.
Keep the following out of v1 unless dual-shape support becomes long-lived enough to justify versioning:
- explicit invoice payload version markers
- consumer-specific schema negotiation for portal, export, reporting, or workflow payloads
- version pinning whose only purpose is to replace documented additive compatibility fields
- forced backfill or re-projection work done only to collapse dual-shape support into one versioned contract
Trigger this follow-on only when:
- dual-shape compatibility remains in place long enough to create meaningful maintenance risk
- downstream consumers need an explicit version handshake instead of additive compatibility fields
- reader rollback and coexistence rules become too implicit to govern safely without a versioned boundary
When that follow-on starts, it must define:
- the authoritative boundary between historical flat invoice payloads and canonical detail-backed invoice payloads
- whether versioning applies only at API boundaries or also to stored export, workflow, and audit projections
- migration and rollback posture when tenants or consumers still need both shapes during the transition
Parity Matrix
The minimum comparison matrix for client-cadence parity must cover the cross-product below before contract cadence is enabled:
| Dimension | Required values |
|---|---|
| Billing frequency | monthly, quarterly, semi-annual, annual, weekly, bi-weekly |
| Due position | advance, arrears |
| Coverage shape | full period, mid-period start, mid-period end, no-coverage |
| Charge family | fixed recurring, recurring product, recurring license, recurring bucket / allowance where timing matters |
| Commercial modifiers | pricing schedules, discounts, custom contract rates, catalog rates |
| Financial overlays | purchase-order required, credits, prepayment, negative invoice follow-on |
| Downstream projections | invoice detail persistence, invoice preview rows, accounting exports, portal / report readers |
The initial fixture set must include at least:
- monthly client cadence anchored mid-month, both advance and arrears
- quarterly, semi-annual, and annual client cadence with anchored month/day behavior
- first partial period and final partial period coverage
- fixed, product, and license recurring families
- pricing schedule override and discount applicability overlays
- PO-required recurring lines
- credit / prepayment / negative invoice scenarios that consume recurring timing
Parity Harness Contract
Pass-0 parity work needs an executable harness contract before cutover work begins. The harness does not require the service-period-first engine to exist yet, but it must define the comparison surface now.
Inputs
- a fixture builder that emits one recurring scenario without requiring invoice persistence as a side effect
- a legacy adapter that can run today's recurring path:
BillingEngine.calculateBilling(...)generateInvoice(...)when persistence comparison is required
- a candidate adapter contract that will run the canonical service-period-first path with the same fixture input
Outputs to compare
- charge identity and family
- service-period boundaries
- due invoice window
- subtotal / tax / total
- invoice detail timing metadata
- downstream export row timing fields where applicable
Blocking vs non-blocking drift
Blocking drift:
- changed charge count
- changed service-period boundaries
- changed due-window selection
- changed amounts, discount coverage, tax basis, or invoice grouping
- changed export- or portal-visible timing meaning
Non-blocking drift during staged rollout:
- additional explainability metadata
- new provenance fields
- additive trace metadata used only for parity comparison or future service-period-first rollout
Staged Rollout Plan
The rollout order must keep client-cadence parity ahead of any contract-cadence capability. The intent is to reduce risk in layers instead of enabling a second recurring clock before the first cutover is proven.
Stage 1 — Additive groundwork only
- Ship shared recurring timing primitives, canonical detail persistence, and client-cadence parity scaffolding with
cadence_ownerdefaulting toclient. - Keep contract cadence blocked on all live write paths; API, service, and UI surfaces may describe the future option, but they must not let tenants persist
cadence_owner = contractyet. - Treat any contract-cadence runtime support in this stage as dark code only. It may exist for tests and isolated selection logic, but it is not tenant-enabled behavior.
Stage 2 — Client-cadence parity comparison
- Run comparison mode for client-cadence recurring lines only.
- Exit criteria for this stage:
- parity harness reports no blocking drift for fixed, product, license, bucket-in-scope, pricing, discount, PO, credit, and export-reader scenarios
- DB-backed client-cadence sanity coverage passes for the representative monthly and longer-frequency fixtures called out in the PRD
- contract cadence remains write-blocked while parity comparison is still required
Stage 3 — Client-cadence cutover
- Make the canonical service-period-first path the live execution path for existing client-cadence tenants.
- Continue treating client billing cycles as the only schedulable recurring run identity in production during this stage.
- Keep contract cadence blocked until client-cadence parity validation is signed off and the post-cutover sanity checks pass.
Stage 4 — Contract-cadence enablement
- Enable contract cadence only after Stage 3 has been stable long enough to prove parity, duplicate prevention, reader hydration, and downstream export behavior on the live client-cadence cutover path.
- When contract cadence is enabled, mixed-cadence selection and grouping must already be deterministic, and unsupported combinations must fail fast rather than falling back to client-cycle behavior.
- Scheduler identity, due-work job payloads, and retry semantics for contract-owned windows remain a separate rollout gate; they must be complete before contract cadence is tenant-writable.
Stage 5 — Cleanup and deletion
- Remove
billing_cycle_alignmentfrom live execution only after both client-cadence parity and contract-cadence rollout are validated. - Remove dead timing helpers and duplicated proration branches only after the validation suite proves no live recurring path still depends on them.
- Preserve rollback posture long enough for historical flat invoices and canonical detail-backed invoices to coexist safely during reader migration.
Fixture Builder Contract
Fixture builders for parity and shared-domain tests should be composable rather than charge-family-specific:
- base client cadence fixture:
- frequency
- anchor / reference date
- invoice window
- cadence owner
- assignment overlay:
- start date
- end date
- recurring obligation overlay:
- charge family
- billing timing
- proration / partial-coverage expectations
- commercial overlay:
- pricing schedule
- discount
- custom rate vs catalog rate
- PO requirement
- financial overlay:
- credit
- prepayment
- negative invoice follow-on
This keeps the future parity harness useful even after fixed recurring, product recurring, and license recurring all share the same canonical timing primitives.