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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
218 lines
13 KiB
Markdown
218 lines
13 KiB
Markdown
# PRD — Contract Template Normalization
|
|
|
|
- Slug: `contract-template-normalization`
|
|
- Date: `2026-03-16`
|
|
- Status: Draft
|
|
|
|
## Summary
|
|
|
|
Normalize the contracts domain so reusable templates exist only in `contract_template*` tables and instantiated client-owned contracts exist only in `contracts` / `contract_lines` / `client_contracts`. Remove the remaining runtime, API, UI, and migration compatibility layers that still treat templates as live contract fallbacks or contract-shaped resources.
|
|
|
|
Make template instantiation the only legal transfer boundary between authoring data and runtime data: templates can be copied into contracts, but live contracts must never read through templates again, and template edits/deletes must never mutate existing contract runtime state.
|
|
|
|
This is a follow-on to `ee/docs/plans/2026-03-16-client-owned-contracts-simplification/`. That plan established the client-owned contract invariant for non-template contracts. This plan finishes the decoupling by eliminating the mixed-model artifacts that still let runtime billing, contract APIs, and legacy scripts behave as if templates and contracts are interchangeable.
|
|
|
|
## Problem
|
|
|
|
The repo currently supports two contradictory realities at once:
|
|
|
|
- The intended architecture says templates are reusable blueprints stored in dedicated `contract_template*` tables, and instantiated contracts are client-owned runtime objects.
|
|
- The implementation still carries multiple compatibility layers from the legacy mixed schema:
|
|
- template rows can still exist in `contracts` and `contract_lines` behind `is_template`
|
|
- client contracts still carry `template_contract_id` as an active runtime lookup key
|
|
- billing still falls back from `client_contracts.contract_id` to `client_contracts.template_contract_id`
|
|
- several actions and UI loaders still map templates into `IContract`-shaped responses
|
|
- template deletion and line repository code still reaches into instantiated contract storage
|
|
|
|
This keeps the system harder to reason about than necessary and introduces real risks:
|
|
|
|
- Billing behavior can still depend on template-side data after a contract has supposedly been instantiated.
|
|
- The codebase still behaves as if a template and its instantiated contracts are linked by a live mutable relationship instead of a one-way copy boundary.
|
|
- Template and contract APIs remain ambiguous, which leaks complexity into every caller.
|
|
- Schema cleanup is blocked because runtime still relies on legacy fallback columns and flags.
|
|
- Operators still need verification scripts that compare “legacy template rows in contracts” to separated template tables, which means the cutover is not finished.
|
|
|
|
## Goals
|
|
|
|
- Make template data authoring-only and one-way copied into instantiated contracts.
|
|
- Make template instantiation the only boundary where authoring data crosses into runtime contracts.
|
|
- Make runtime billing and discount resolution depend only on instantiated contract/client assignment state.
|
|
- Stop treating templates as `IContract`-shaped runtime resources in APIs and UI loaders.
|
|
- Remove the remaining behavioral dependence on `contracts.is_template`, `contract_lines.is_template`, and template-backed runtime fallbacks.
|
|
- Reduce `client_contracts.template_contract_id` to provenance-only metadata or remove it entirely if no longer needed.
|
|
- Retire legacy verification/backfill scripts that assume duplicated storage once cutover is complete.
|
|
- Leave the codebase with one clear model:
|
|
- templates define reusable defaults
|
|
- contracts and client contracts define live billing behavior
|
|
|
|
## Non-goals
|
|
|
|
- Reversiting the client-owned contracts migration already delivered in the March 16 simplification plan.
|
|
- Redesigning the contract template product experience.
|
|
- Rewriting historical invoices or moving invoice history to template-aware tables.
|
|
- Changing the core contract billing semantics, tax calculation semantics, or renewal business rules beyond removing template fallback.
|
|
- General contract-domain redesign outside contract/template normalization.
|
|
- Adding observability, metrics, rollout toggles, or operational tooling beyond what is required to validate the cutover.
|
|
|
|
## Users and Primary Flows
|
|
|
|
- Billing admin
|
|
1. Creates or edits reusable templates in template-specific screens.
|
|
2. Creates client-owned contracts from templates or from scratch.
|
|
3. Bills clients from instantiated contract data without template-side fallback.
|
|
|
|
- Finance / operations
|
|
1. Reviews live contracts and invoices without needing to understand template linkage.
|
|
2. Runs billing/reporting flows that operate on instantiated contract facts only.
|
|
|
|
- Engineers / support
|
|
1. Can trace a bug to either template authoring or contract runtime behavior without a mixed resource model.
|
|
2. Can reason about template provenance separately from live billing state.
|
|
|
|
## UX / UI Notes
|
|
|
|
- Templates and contracts should stop sharing a single implicit “contract detail” abstraction.
|
|
- Template screens should load template DTOs and template lines directly, not contract-shaped surrogates.
|
|
- Contract screens should load only client-owned contract headers and assignment/runtime data.
|
|
- `getContractById`-style flows should not silently return a template when a contract lookup misses.
|
|
- Any UI still showing `is_template` as a first-class branch on the same runtime type should be replaced with explicit route/view separation.
|
|
- Template edit/delete affordances should clearly operate on reusable authoring assets, not on already-instantiated contracts.
|
|
|
|
## Requirements
|
|
|
|
### Functional Requirements
|
|
|
|
- Single-write instantiation boundary
|
|
- Template instantiation must be the only supported path for moving template-defined data into runtime contract tables.
|
|
- After instantiation, runtime billing, reporting, and contract CRUD must treat instantiated rows as self-contained facts.
|
|
- Template edits must not propagate into existing contracts unless an explicit reapply/reclone workflow is intentionally built later.
|
|
- Template deletes must only affect authoring-side records and provenance references, never live contract behavior or runtime rows.
|
|
|
|
- Runtime/billing decoupling
|
|
- Billing engine contract-line resolution must load only from instantiated contract IDs.
|
|
- Billing discount resolution must not join through `template_contract_id` fallbacks.
|
|
- Cloned contract data must remain billable even if the source template changes later.
|
|
|
|
- Template provenance
|
|
- Decide and document whether `client_contracts.template_contract_id` remains as provenance metadata.
|
|
- If retained, it must not be used as a live billing/configuration fallback.
|
|
- If dropped, migration and API flows must preserve enough provenance elsewhere for audit/debug needs.
|
|
|
|
- Contract/template API separation
|
|
- Contract list/detail APIs must only return instantiated contracts.
|
|
- Template list/detail APIs must only return template resources.
|
|
- Shared DTO adapters that map templates into `IContract`-shaped responses must be removed or isolated behind explicit compatibility endpoints slated for deletion.
|
|
|
|
- Repository/action separation
|
|
- Contract line repositories/actions must stop accepting a single “maybe template, maybe contract” ID space.
|
|
- Template line CRUD and contract line CRUD must be separated at the repository/action boundary.
|
|
- Template delete/update flows must not mutate instantiated contract tables.
|
|
|
|
- Schema cleanup
|
|
- Remove live reliance on `contracts.is_template`.
|
|
- Remove live reliance on `contract_lines.is_template`.
|
|
- Remove or re-scope legacy verification scripts that compare duplicated legacy template rows with dedicated template tables.
|
|
- After runtime cutover and validation, add cleanup migration(s) that drop obsolete columns / legacy rows / compatibility references.
|
|
|
|
- Data validation and migration safety
|
|
- Before destructive cleanup, validate that all legacy template rows in `contracts` and `contract_lines` have equivalent canonical records in `contract_template*`.
|
|
- Fail closed if any tenant still relies on legacy-only template data or missing canonical template records.
|
|
- Provide a deterministic operator runbook for preflight, cutover, and post-cutover verification.
|
|
|
|
### Non-functional Requirements
|
|
|
|
- Cutover must be staged so runtime behavior changes land before schema deletion.
|
|
- Validation must rely on real database reads against migrated schema, not only source-string tests.
|
|
- The resulting architecture must be simpler than the current one: no hidden runtime fallback from live contracts to templates.
|
|
- The resulting architecture must have a single, obvious data-flow direction: template authoring data can be copied into runtime contracts, but runtime contracts do not read back through template state.
|
|
- The cleanup must remain compatible with the current multi-tenant / Citus environment constraints.
|
|
|
|
## Data / API / Integrations
|
|
|
|
- Template source of truth
|
|
- `contract_templates`
|
|
- `contract_template_lines`
|
|
- `contract_template_line_*`
|
|
|
|
- Runtime source of truth
|
|
- `contracts`
|
|
- `contract_lines`
|
|
- `client_contracts`
|
|
- `contract_pricing_schedules`
|
|
- contract-line child/config tables
|
|
|
|
- Required cleanup targets
|
|
- `contracts.is_template`
|
|
- `contract_lines.is_template`
|
|
- any runtime `coalesce(template_contract_id, contract_id)` joins
|
|
- any OR joins on `template_contract_id` vs `contract_id`
|
|
- contract/template action adapters that flatten templates into contract DTOs
|
|
|
|
- Scripts and verification
|
|
- `server/scripts/verify-template-migration.ts` currently assumes duplicated storage and will need to become either:
|
|
- a strict pre-cutover verifier only, or
|
|
- a cleanup verifier that proves legacy rows are gone and canonical template rows remain
|
|
- `server/scripts/contract-template-decoupling.ts` currently preserves hybrid semantics and should be retired or rewritten to match the normalized model
|
|
|
|
- External/accounting integrations
|
|
- No intentional behavior change to invoice export payloads, but invoice generation must continue to derive from instantiated invoices/contracts only.
|
|
- If any downstream export uses template provenance today, document and preserve that explicitly rather than via hidden fallback joins.
|
|
|
|
## Security / Permissions
|
|
|
|
- No new roles or permissions are required.
|
|
- Existing billing/contract/template permissions remain in force.
|
|
- API separation must not accidentally broaden template visibility through contract endpoints or vice versa.
|
|
|
|
## Observability
|
|
|
|
- Out of scope as a product feature.
|
|
- Migration/cutover scripts must log sufficient validation output to identify blocking tenants and mismatched legacy/template records.
|
|
|
|
## Rollout / Migration
|
|
|
|
- Phase 0: inventory and invariants
|
|
- inventory all runtime uses of `template_contract_id`, `contracts.is_template`, and `contract_lines.is_template`
|
|
- inventory all API/UI contract-template compatibility adapters
|
|
- verify canonical template data exists for every legacy template row
|
|
|
|
- Phase 1: runtime cutover
|
|
- codify template instantiation as the sole transfer boundary from authoring data to runtime data
|
|
- remove billing-engine and discount fallback to template-backed contract IDs
|
|
- ensure all template-to-contract instantiation paths fully clone required data into runtime tables
|
|
- validate invoice generation and discount application on instantiated contracts only
|
|
|
|
- Phase 2: API/UI and repository separation
|
|
- split template DTOs/routes/actions from contract DTOs/routes/actions
|
|
- split template-line CRUD from contract-line CRUD
|
|
- remove “contract lookup falls back to template lookup” behavior
|
|
|
|
- Phase 3: schema and script cleanup
|
|
- stop relying on duplicated legacy template rows
|
|
- retire or rewrite legacy backfill/verification scripts
|
|
- drop obsolete legacy columns / rows once preconditions are satisfied
|
|
|
|
- Phase 4: post-cutover validation
|
|
- verify no runtime code path joins templates as a live billing source
|
|
- verify no contract endpoint returns template-backed data
|
|
- verify deleting/updating a template cannot mutate live contract rows
|
|
|
|
## Open Questions
|
|
|
|
- Should `client_contracts.template_contract_id` remain as immutable provenance metadata, or should provenance move to a different field/table before removal?
|
|
- Do any production tenants still depend on template rows remaining present in `contracts` for operational tooling outside the app?
|
|
- Should the final cleanup physically delete legacy template rows from `contracts`/`contract_lines`, or mark them unreachable first and purge later?
|
|
- Do we want a temporary compatibility API for callers currently expecting templates in contract-shaped payloads, or should we cut those consumers over directly?
|
|
|
|
## Acceptance Criteria (Definition of Done)
|
|
|
|
- Billing and discount resolution no longer fall back from `client_contracts.contract_id` to template-backed IDs.
|
|
- Template instantiation is the only supported authoring-to-runtime transfer path, with no reverse sync or live template reads after assignment.
|
|
- Templates are no longer returned from contract endpoints or contract lookups.
|
|
- Template CRUD no longer mutates instantiated contract tables.
|
|
- Editing or deleting a template after contract instantiation does not change live contract runtime behavior.
|
|
- Contract line CRUD and template line CRUD are separated in repositories/actions and no longer share a mixed “contract or template” code path.
|
|
- Canonical template records fully replace legacy template rows as the reusable source of truth.
|
|
- Legacy compatibility scripts/paths are either removed or rewritten to validate the normalized model.
|
|
- The system can safely drop obsolete legacy template markers/columns without changing runtime behavior.
|