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

15 KiB

PRD — Recurring Invoicing Hard Cutover

  • Slug: recurring-invoicing-hard-cutover
  • Date: 2026-03-18
  • Status: Draft

Summary

Finish the recurring-billing cutover by removing the remaining bridge assumptions that still treat billing_cycle_id and client_billing_cycles as the primary substrate for recurring invoice execution.

This plan is narrower than 2026-03-16-service-period-first-billing-and-cadence-ownership, but stricter than 2026-03-18-service-driven-invoicing-cutover. It assumes the system already has:

  • cadence_owner
  • persisted recurring_service_periods
  • selector-input recurring execution
  • recurring invoice linkage back to service-period rows

The remaining problem is that the application still carries a large compatibility layer that allows recurring work to pretend it is billing-cycle-driven. That layer adds complexity, hides real data issues, and creates product ambiguity.

The target end state is:

  • recurring due work comes only from recurring_service_periods
  • recurring execution identity is service-period / execution-window based only
  • recurring preview/generate/run/history/reversal do not require or infer truth from billing_cycle_id
  • client_billing_cycles remain valid client cadence records, but only as source rules for cadence_owner = client and as optional historical metadata

Problem

The codebase is in an uncomfortable middle state:

  • the billing engine can already operate on canonical recurring execution windows
  • recurring service periods can already be materialized and linked to invoice detail rows
  • the UI and API can already represent some bridge-less recurring invoices

but many surrounding behaviors still preserve the old recurring model:

  • due-work readers synthesize compatibility rows from client_billing_cycles
  • missing service-period materialization is treated as a fallback path instead of a repairable failure
  • duplicate prevention, reruns, and invoice identity still prefer billing_cycle_id
  • recurring run orchestration still accepts cycle IDs as the operational handle
  • history and reversal still model recurring invoice behavior as billing-cycle operations
  • API contracts still make billing_cycle_id a first-class recurring request shape
  • invoice reads still infer recurring semantics from invoices.billing_cycle_id
  • accounting export and some invoice-linkage paths still preserve mixed canonical/fallback recurring provenance
  • some business logic still treats billing_cycle_id = null as a proxy for unrelated invoice classes such as prepayments

This imposes a real complexity tax:

  • more branching
  • more migration-only tolerance in steady-state code
  • more room for silent misclassification
  • more operator confusion
  • more tests spent preserving replaced behaviors

Goals

  • Make recurring service periods the only source of ready recurring invoice work.
  • Remove recurring runtime dependence on billing_cycle_id.
  • Remove recurring compatibility fallbacks that synthesize or tolerate client-cycle-based recurring work after service-period cutover.
  • Keep client_billing_cycles only for legitimate client cadence administration, source-rule generation, and optional read-side historical context.
  • Make recurring preview/generate/run/history/reverse/delete uniformly keyed by execution-window or service-period identity.
  • Remove mixed-schema guards for recurring service-period structures.
  • Remove recurring compatibility DTOs and request contracts whose only purpose is to preserve the billing-cycle bridge.
  • Reclassify invoice kinds using explicit fields instead of null/non-null billing_cycle_id.
  • Document the final recurring mental model clearly enough that future code does not regress.

Non-goals

  • Removing client billing schedules from the product.
  • Removing client_billing_cycles as a table if it is still needed for client cadence management or non-recurring/client-schedule operations.
  • Rewriting historical invoice records beyond what is necessary for consistent read models.
  • Redesigning the broader cadence-ownership model or service-period materialization model.
  • Removing billing-cycle-based APIs that are legitimately about client billing schedule administration rather than recurring invoice execution.
  • Reworking manual invoice flows except where they are misclassified by the recurring bridge assumptions.

Core Invariant

Recurring invoicing must obey this invariant:

  1. A recurring obligation has a cadence owner.
  2. The cadence owner and source rules generate persisted recurring service periods.
  3. Ready recurring work is the set of due recurring service-period records.
  4. Recurring invoice preview and generation operate on execution-window/service-period identity only.
  5. Billed recurring invoices are understood historically from linked recurring service-period records and canonical recurring detail periods.
  6. billing_cycle_id is never required to decide what recurring work exists, whether it is duplicate, how it should be previewed, how it should be generated, or how it should be reversed.

Legitimate Concepts To Keep

These concepts remain valid:

  • client billing schedule configuration
  • client cadence anchors and previews
  • client cadence as the source rule when cadence_owner = client
  • optional invoice header metadata indicating a client-cycle bridge existed
  • historical read-side enrichment by billing cycle where helpful

These concepts must stop driving recurring semantics:

  • billing_cycle_id as recurring execution identity
  • client_billing_cycles as the universal recurring due-work substrate
  • compatibility rows that fabricate recurring work from billing cycles
  • schema guards that hide missing recurring service-period structures
  • history/reversal APIs framed as “billing cycle” operations for recurring invoices

Users and Primary Flows

  • Billing admin

    1. Opens recurring invoicing and sees due work sourced directly from recurring service periods.
    2. Previews and generates recurring invoices from execution-window/service-period identity.
    3. Reverses or deletes recurring invoices through service-period linkage repair, not through billing-cycle semantics.
  • Finance / operations

    1. Understands recurring invoice history from canonical service periods.
    2. Does not need to know whether a billing-cycle bridge existed to reason about recurring work.
    3. Sees materialization failures as repairable service-period issues, not fallback-ready invoice rows.
  • Developer / maintainer

    1. Reads one recurring model instead of a canonical model plus compatibility branch.
    2. Can change recurring runtime behavior without auditing for legacy billing-cycle fallback paths.

UX / UI Notes

  • AutomaticInvoices should be a recurring service-period execution surface.
  • Client cadence rows for recurring work should appear only because they are materialized recurring service periods whose cadence owner is client, not because a client_billing_cycles row exists.
  • Recurring invoice history should stop being presented as “invoiced billing cycles.”
  • Reverse/delete affordances should describe service-period linkage and invoice effects directly.
  • Operator-facing service-period gaps should become explicit errors or repair actions, not compatibility rows.
  • Authoring UI may still default cadence_owner to client if that is the product choice, but that must be an explicit UX default, not a runtime fallback.

System Surfaces In Scope

  • Billing dashboard recurring UI

    • AutomaticInvoices
    • recurring history views
    • recurring service-period review/manage surfaces
  • Billing actions

    • due-work readers
    • recurring run selection/orchestration
    • invoice preview/generation
    • reversal/delete flows
    • invoice modification / invoice kind logic
  • Runtime / engine / linkage

    • billing engine recurring selection
    • invoice charge/detail linkage back to recurring service periods
    • duplicate prevention
    • billed-through and rerun logic
    • bucket recurring period resolution
  • API / shared contracts

    • invoice schemas/controllers/services
    • shared recurring timing and invoice interfaces
    • financial schemas that still expose recurring billing-cycle request shapes
  • Read models / exports

    • invoice queries
    • invoice service history/list/detail projections
    • accounting export recurring period provenance
  • Migrations / cleanup

    • recurring-service-period required schema assumptions
    • possible retirement/deprecation path for invoices.billing_cycle_id as recurring runtime data

Functional Requirements

  1. The recurring due-work reader must load due rows only from recurring_service_periods.
  2. Client-cadence recurring rows must be representable without a required billing_cycle_id.
  3. Missing recurring service-period materialization must be treated as a failure/repair state, not a compatibility invoice row.
  4. Mixed-schema guards for missing recurring service-period tables/columns must be removed from recurring invoice paths.
  5. Recurring execution identity must be expressible without billingCycleId.
  6. Client-cadence execution identity must be derived from canonical schedule/window/service-period identity, not a cycle UUID.
  7. Recurring run target selection must operate on canonical due-work rows only.
  8. Recurring job payloads/handlers must accept canonical recurring execution identity only.
  9. Recurring preview must accept canonical selector input only.
  10. Recurring generate must accept canonical selector input only.
  11. Compatibility request wrappers that preserve billing_cycle_id as a recurring API option must be removed from recurring-facing contracts.
  12. Duplicate prevention for recurring invoices must use canonical execution-window/service-period identity and linked rows, not invoices.billing_cycle_id.
  13. Invoice insertion for recurring work must not require or derive a billing-cycle bridge.
  14. Recurring invoice-linkage repair must not branch on whether the invoice header has billing_cycle_id.
  15. Billed recurring detail rows must link back to recurring service-period records using canonical identity only.
  16. Recurring history queries must be invoice/service-period based, not billing-cycle based.
  17. Recurring reversal/delete operations must repair recurring service-period linkage and lifecycle state without treating billing cycles as the primary object.
  18. Any recurring action naming or UI copy that still frames the object as a billing cycle must be updated.
  19. Recurring invoice kind classification must not use null/non-null billing_cycle_id as a proxy for prepayment or non-recurring behavior.
  20. Invoice list/read logic must stop inferring “recurring” from invoices.billing_cycle_id.
  21. Recurring invoice DTOs must stop carrying bridge-only recurring fields as first-class semantics.
  22. Canonical recurring detail periods must be the recurring read model; fallback recurring projection layers should be removed where they only preserve bridge logic.
  23. Accounting export must use canonical recurring detail/service-period data only for recurring invoices.
  24. Bucket recurring period resolution must align with canonical recurring service periods instead of preferring client_billing_cycles.
  25. Client billing schedule changes must regenerate future recurring service periods rather than relying on future billing-cycle row mutation to define recurring work.
  26. Client billing schedule APIs and UI that are not about recurring execution may remain.
  27. The final recurring architecture must be documented clearly enough that future code does not reintroduce the bridge model.

Non-functional Requirements

  • The cutover should simplify, not merely relocate, compatibility branches.
  • Errors that were previously hidden by compatibility fallbacks should fail explicitly and diagnostically.
  • Historical invoices remain readable even if their bridge metadata is retained only as optional context.
  • Query and type contracts should make recurring identity obvious and difficult to misuse.
  • Test coverage must prove that client-cadence recurring execution still works after bridge removal.

Data / API / Integrations

  • Recurring API request contracts should standardize on canonical selector input.
  • billing_cycle_id may remain in invoice headers and read models only as optional historical/client-context metadata.
  • Shared interfaces should separate:
    • client billing schedule models
    • recurring execution identity
    • recurring history/detail metadata
  • Accounting export should treat canonical recurring detail periods as the only recurring period source.
  • Migration/cleanup may need to deprecate or eventually drop invoices.billing_cycle_id from recurring-specific logic even if the column remains temporarily for history.

Rollout / Migration

This is a hard-cutover plan, not a coexistence plan.

Expected sequence:

  1. Remove bridge assumptions from contracts and helpers first.
  2. Cut due-work, recurring runs, preview, and generation to canonical identity only.
  3. Rework history, reversal/delete, and read models.
  4. Remove compatibility fallbacks and mixed-schema guards.
  5. Clean up DTOs, exports, authoring compatibility shims, and invoice-kind misuse.
  6. Validate that client-cadence recurring execution still works entirely through service periods.

The plan should identify every place where a temporary bridge must either:

  • remain only as passive metadata
  • or be removed entirely

Risks

  • Client-cadence recurring behavior may still rely on hidden billing_cycle_id assumptions in duplicate detection or rerun logic.
  • Historical recurring invoices may have incomplete canonical linkage and need read-side fallback during cleanup.
  • Some non-recurring flows may currently piggyback on billing_cycle_id nullability and need an explicit invoice-kind model before recurring cleanup lands.
  • Accounting export and bucket logic may have more legacy cycle assumptions than the main invoicing UI.

Open Questions

  • Should invoices.billing_cycle_id remain as passive historical metadata indefinitely, or should there be a later physical removal plan?
  • For history reads, how much fallback to incomplete historical recurring linkage is acceptable if it no longer shapes live recurring behavior?
  • Should explicit invoice kind/type be introduced as a dedicated field now, or can prepayment/misc recurring misclassification be corrected with existing fields?

Acceptance Criteria

  • Ready recurring work is sourced only from recurring_service_periods.
  • Recurring preview/generate/run flows accept only canonical recurring selector input.
  • Recurring client-cadence execution still works without requiring billing_cycle_id.
  • Recurring duplicate detection, linkage, and rerun logic are canonical and bridge-free.
  • Recurring history, reverse, and delete operations are no longer modeled as billing-cycle operations.
  • Recurring read models and exports derive recurring semantics from canonical service-period/detail data rather than billing_cycle_id.
  • Mixed-schema guards and compatibility fallback rows are gone from recurring invoice paths.
  • The remaining role of client_billing_cycles is clearly limited to cadence management/source rules and optional historical context.