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
12 KiB
12 KiB
PRD — Service Catalog Billing Mode Decoupling
- Slug:
service-catalog-billing-mode-decoupling - Date:
2026-03-21 - Status: Draft (Hard Cutover)
Summary
Decouple service identity from billing behavior so the same catalog service can be billed differently per contract line (fixed, hourly, usage) while still supporting default pricing. Remove hard gating that currently treats service_catalog.billing_method as eligibility truth, enforce allocation by contract-line service membership, and make non-contract time/usage explicit instead of silently sweeping null-linked entries into arbitrary contract lines.
Problem
- Today, contract authoring gates services by
service_catalog.billing_method, so a service markedhourlycannot be added under fixed, even when the business contract requires fixed pricing for that same service. - Billing engine still includes
contract_line_id IS NULLtime/usage in each contract-line calculation pass, which can cause ambiguous or duplicate allocation behavior. - API and UI surfaces conflate catalog metadata with billing behavior and expose
billing_methodas if it is immutable service identity. - Product teams need the model to be:
item_kinddescribes what it is (servicevsproduct).- contract line mode describes how it is billed.
- catalog can provide optional mode-specific defaults.
Goals
- Allow any
item_kind='service'service to be added to fixed/hourly/usage contract-line contexts. - Make contract-line context the authoritative billing mode, not catalog
billing_method. - Preserve defaulting ergonomics via mode-specific default prices/rates in catalog metadata.
- Ensure time/usage allocation only bills through valid matching contract-line services.
- Treat unresolved (non-contract) time/usage as explicit work, not implicit fallback.
- Keep existing invoice persistence semantics intact (including mixed assignment support), while giving deterministic allocation.
Non-goals
- Replacing the full invoice data model.
- Reworking tax/export/GL behavior.
- Replacing service types taxonomy (
custom_service_type_idremains identity taxonomy). - Full redesign of manual invoice creation UX.
- Changing permission model for service catalog CRUD.
Users and Primary Flows
- Billing admin configures a catalog service once and optionally sets default prices per billing mode.
- Contract author creates/edits contract lines and can add any service under fixed/hourly/usage sections; defaults prefill but remain editable.
- Technician enters time with service; explicit contract-line assignment is preferred but not required.
- Billing engine allocates approved uninvoiced records:
- explicit line assignment first,
- unique service-based contract match second,
- unresolved remains non-contract work.
- Invoicing user can bill contract-backed work and non-contract work separately, and optionally combine only when compatible.
UX / UI Notes
- Contract wizard/service pickers:
- remove hard
billingMethodsgating for service items by section, - still gate
item_kindwhere needed (servicevsproduct).
- In each section, show “Default for this mode” when a mode-specific default exists.
- Contract-line service forms should display effective mode + source of default (
catalog default,contract override,none). - Time entry contract info banner should clearly show:
- assigned contract line,
- uniquely inferred contract line,
- unresolved non-contract billing path.
- Automatic invoicing grouped UI should represent non-contract candidates as first-class items.
Requirements
Functional Requirements
L0 Objective
Establish a single behavioral rule: service identity is catalog-level; billing mode is contract-line-level.
L1.A Data Model and Vocabulary
- Keep
item_kindas identity discriminator. - Introduce a mode-specific default-rate structure keyed by:
service_id,billing_mode,currency_code,- tenant.
- Canonicalize vocabulary to
fixed | hourly | usageand remove activeper_unitwrites. - Do not preserve compatibility reads for legacy fields after cutover migration is applied.
L1.B Contract Authoring and Mutation
- Wizard/template authoring must no longer reject services based on catalog
billing_method. - Server-side submission validation must enforce contract context requirements, not catalog-method matching.
- Adjacent APIs (
add service to contract line, preset/template line service actions) must align with same policy.
L1.C Pricing Defaults
- Default selection precedence for contract-line service config:
- explicit contract override,
- catalog mode default for contract currency,
- no default (user must enter/confirm).
- Defaults must be applied consistently in wizard, template wizard, and line-edit screens.
L1.D Time/Usage Allocation
- Replace unconditional null-line fallback with service-aware allocation.
- Allocation precedence:
- explicit
contract_line_id, - unique eligible active contract-line service match for
(client, service, date), - unresolved non-contract.
- Never allocate a record to a contract line that does not include the record’s service.
L1.E Invoicing Behavior
- Non-contract approved billable records must be selectable as explicit invoice candidates.
- Contract-backed and non-contract candidates can be generated separately.
- Combination is allowed only under compatibility rules (client/window/currency/tax/export/PO scope).
L1.F API/Schema Compatibility
- Hard-cutover API contracts and remove legacy alias fields (including
service_typecompatibility fields). - Update Zod/api/interface contracts that currently require catalog
billing_methodas behavioral truth.
L1.G Migration and Backfill
- Backfill mode defaults from current catalog data into new default-rate structure.
- Normalize
per_unitlegacy values tousagein writes and validation. - Migration is one-way; no dual-read/dual-write compatibility path is retained.
Non-functional Requirements
- Deterministic allocation: same input set must always produce same contract/non-contract partition.
- No regression in existing cadence/materialization paths introduced by this plan.
- DB-backed integration tests required for new reads/writes and migration backfill behavior.
- No hidden fallback paths that silently remap unresolved records.
Data / API / Integrations
- Primary touched areas:
packages/billing/src/actions/contractWizardActions.tspackages/billing/src/actions/contractLineServiceActions.tspackages/billing/src/lib/billing/billingEngine.tspackages/scheduling/src/actions/timeEntryCrudActions.tspackages/billing/src/actions/serviceActions.tsserver/src/lib/api/services/ServiceCatalogService.tsserver/src/lib/api/services/ProductCatalogService.ts
- Add/update schema contracts:
server/src/lib/api/schemas/serviceSchemas.tsserver/src/lib/api/schemas/productSchemas.tsserver/src/lib/api/schemas/financialSchemas.tsserver/src/lib/api/schemas/contractLineSchemas.ts
- Identity handling alignment:
- assignment-scoped IDs already used in clients stack,
- billing-engine readers must handle/parse correctly where needed.
Security / Permissions
- No new permissions introduced.
- Existing service catalog and contract authoring permissions remain unchanged.
Observability
- Not adding new telemetry scope in this plan.
- Error messages for unresolved allocation must remain actionable and non-ambiguous.
Rollout / Migration
- Phase 0: apply one-way schema migration/backfill and canonical value normalization.
- Phase 1: update wizard/picker/API validations to contract-context semantics.
- Phase 2: update engine allocation to service-aware matching and explicit non-contract outputs.
- Phase 3: land strict schema/API hard cutover and remove all compatibility branches in same release.
Execution Order
Wave 0 — Schema and Canonicalization Gate
- Scope:
F001-F003. - Entry criteria:
- migration scripts authored,
- backfill mapping rules documented.
- Exit criteria:
- canonical vocabulary enforced in writes,
- mode-default storage created and populated,
- migration tests green (
T001-T005).
- Stop-the-line conditions:
- residual
per_unitwrite paths remain, - backfill cannot produce complete defaults for active services.
Wave 1 — Contract Authoring Cutover
- Scope:
F004-F017. - Depends on: Wave 0.
- Entry criteria:
- schema is migrated in dev/test DB,
- default resolver available to wizard/form code.
- Exit criteria:
- wizard/template and line service actions use contract-context validation only,
- no catalog-method eligibility gates remain in these paths,
- prefill behavior is deterministic for fixed/hourly/usage,
- tests green (
T006-T027).
- Stop-the-line conditions:
- contract creation/edit can still fail solely due to catalog
billing_method, - wizard resume loses or mutates selected service/rate state.
Wave 2 — Engine Allocation Integrity
- Scope:
F018-F025. - Depends on: Wave 1.
- Entry criteria:
- contract authoring can produce decoupled line/service mappings reliably.
- Exit criteria:
- unconditional null-line fallbacks removed for time and usage,
- service-membership-constrained allocation enforced,
- pricing and bucket regressions absent,
- tests green (
T028-T040).
- Stop-the-line conditions:
- same unassigned record can be billed by multiple lines,
- rounding/minimum/overtime/tiering regressions are detected.
Wave 3 — Invoicing Candidate and Generation Behavior
- Scope:
F026-F032. - Depends on: Wave 2.
- Entry criteria:
- engine returns deterministic contract/non-contract partitioning.
- Exit criteria:
- non-contract candidates appear as first-class due work,
- separate vs combined generation works with compatibility guards,
- preview/generate summary accuracy verified,
- tests green (
T041-T049).
- Stop-the-line conditions:
- non-contract work is invisible/inaccessible,
- incompatible mixed selections combine incorrectly.
Wave 4 — API/Schema/Downstream Hard Cutover
- Scope:
F033-F043. - Depends on: Wave 3.
- Entry criteria:
- core behavior is stable in billing flows.
- Exit criteria:
- all affected APIs/schemas/interfaces reflect decoupled semantics,
- no legacy alias contracts retained,
- onboarding/settings/usage tracking callers aligned,
- tests green (
T050-T060).
- Stop-the-line conditions:
- any consumer still requires legacy alias payloads,
- compile/schema suites fail due to mixed old/new contracts.
Wave 5 — Final Debt Purge and Guard Rails
- Scope:
F044. - Depends on: Wave 4.
- Entry criteria:
- all functional paths migrated.
- Exit criteria:
- compatibility/fallback branches removed,
- static guards prevent reintroduction,
- e2e bootstrap no longer injects stale constraints,
- final DB-backed sanity run passes,
- tests green (
T061-T066).
- Stop-the-line conditions:
- any lingering compatibility branch is still executed in production paths,
- static guards fail to catch reintroduced legacy gates/fallbacks.
Open Questions
- Resolved (2026-03-21): Canonical billing mode vocabulary is
fixed | hourly | usage;per_unitis legacy compatibility only. - Resolved (2026-03-21): Source of billing behavior truth is contract-line context; catalog stores defaults, not enforcement.
- Resolved (2026-03-21): Products remain
item_kind='product'; product billing behavior is handled in product line flows and not used to gate service line eligibility. - Resolved (2026-03-21): Non-contract time/usage must be first-class selectable invoice candidates, not implicit sweep-in.
- Resolved (2026-03-21): No compatibility compromises: no dual-read/dual-write and no transitional alias fields retained post-migration.
Acceptance Criteria (Definition of Done)
- A single service can be added under fixed or hourly contract sections without catalog-method rejection.
- Wizard/template and line-edit actions no longer hard-fail on catalog
billing_methodmismatch for services. - Mode-specific defaults prefill rates in each contract-line context and remain editable.
- Billing engine no longer uses unconditional
contract_line_id IS NULLfallback in a way that can multi-claim records. - Unresolved approved billable time/usage appears as explicit non-contract invoice candidates.
- Users can invoice contract-backed and non-contract work separately; combined generation only occurs when compatibility checks pass.
- API schemas/interfaces are consistent with decoupled semantics and tests are green.
- Legacy alias fields and compatibility branches are removed in the same cutover release.
- Migration/backfill tests prove existing data remains billable and deterministic post-cutover.