Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
23 KiB
Scratchpad — Service Catalog Billing Mode Decoupling
- Plan slug:
service-catalog-billing-mode-decoupling - Created:
2026-03-21
What This Is
Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing this plan.
Prefer short bullets. Append new entries as you learn things, and also update earlier notes when a decision changes or an open question is resolved.
Decisions
- (2026-03-21) Canonical billing vocabulary is
fixed|hourly|usage;per_unitis compatibility-only. Rationale: current migrations already moved towardusage, while product APIs still emitper_unit; plan must converge on one canonical set. - (2026-03-21) Service catalog defines identity and optional defaults; contract-line context defines billing behavior. Rationale: same service must be billable differently per agreement.
- (2026-03-21) Non-contract time/usage is explicit invoiceable scope, not implicit fallback absorption into contract lines. Rationale: deterministic allocation and user control over separate invoicing.
- (2026-03-21) Hard cutover selected: no dual-read/dual-write and no transitional alias retention after migration. Rationale: avoid long-term complexity debt and hidden fallback regressions.
- (2026-03-21) Wave 0 canonicalization starts by rejecting
per_unitat server write-schema boundaries (serviceSchemas,financialSchemas,contractLineSchemas) and hardening migration post-conditions. Rationale: immediate write-path guardrails plus deterministic migration safety without waiting for downstream UI/type cleanup waves. - (2026-03-21) Mode-specific catalog defaults are stored in a dedicated table (
service_catalog_mode_defaults) rather than overloading existingservice_prices. Rationale: keep contract-mode defaults explicit and avoid breaking multi-currency price reads while migration/backfill work lands. - (2026-03-21) Backfill semantics for mode-defaults are one-way and service-scoped: seed from
service_pricesfirst, fallback toservice_catalog.default_rateonly when no per-currency rows exist, and fail on unresolved active-service mappings. Rationale: preserve the richest existing pricing data while preventing silent gaps. - (2026-03-21) Wizard and template service pickers now apply only
itemKinds=['service']gating for fixed/hourly/usage sections; billing-method filter arguments were removed from these step-level picker calls. Rationale: service identity is catalog-level and billing mode is contract-context. - (2026-03-21) Contract/template wizard submit validators now enforce only
item_kind='service'for fixed/hourly/usage service arrays; billing_method matching checks were removed from server-side submission validation. - (2026-03-21)
addServiceToContractLinenow determines default configuration type from targetcontract_line_type(Fixed/Hourly/Usage) and validates explicitconfigTypecompatibility per line mode, instead of deriving from catalog/service-type billing method.
Discoveries / Constraints
- (2026-03-21) Wizard gates services by
billing_methodin picker and server submit validation. Key refs:ServiceCatalogPicker,FixedFeeServicesStep,HourlyServicesStep,UsageBasedServicesStep,contractWizardActions. - (2026-03-21)
contractLineServiceActionsstill enforces service/type billing-mode coupling in add/attach paths, so wizard-only changes are insufficient. - (2026-03-21) Billing engine still includes
contract_line_id IS NULLin time/usage queries per contract-line pass, which can multi-claim unresolved rows without service-aware guard. - (2026-03-21)
time_entriesdefault tocontract_line_id = nullin save paths, so allocation correctness must be enforced centrally at billing execution. - (2026-03-21) Many downstream APIs and schemas require/expose
billing_methodas identity truth (serviceSchemas,productSchemas,financialSchemas, shared interfaces, scheduling aliases); all are in-scope for one-shot cutover updates. - (2026-03-21) E2E setup currently injects legacy billing-method constraints (
per_unitetc.), which can mask decoupled behavior regressions. - (2026-03-21) Existing migration
20251016120000_update_billing_method_to_text.cjsnormalizedper_unittousagebut did not fail when residual legacy rows remained; added explicit residual-count guard + throw. - (2026-03-21) Product flows still actively write
billing_method: 'per_unit'(ProductCatalogService,productSchemas), so canonicalization for product writes remains follow-up inF037/F039. - (2026-03-21) Added migration
20260321110000_create_service_catalog_mode_defaults.cjswith tenant+service scoped FK, strict unique key(tenant, service_id, billing_mode, currency_code), billing_mode check (fixed|hourly|usage), and non-negative rate check. - (2026-03-21) Added migration
20260321113000_backfill_service_catalog_mode_defaults.cjsthat normalizesper_unit -> usage, inserts fromservice_prices, falls back toservice_catalogdefaults, and throws when active services with source defaults still have no mode-default rows. - (2026-03-21)
ServiceCatalogPickerstill supports optionalbilling_methods, but wizard and template steps no longer pass billing-method filters in fixed/hourly/usage service flows; only products step still usesitemKinds=['product']as expected. - (2026-03-21) Added integration coverage (
T013-T018) inserver/src/test/integration/contractWizard.integration.test.tsfor decoupled acceptance (fixed/hourly/usage) and non-service rejections. - (2026-03-21) Removed
service_typesjoin andservice_type_billing_methoddependency incontractLineServiceActionsattach flow; compatibility now enforced via target line mode +allowedConfigTypesByPlan.
Commands / Runbooks
- (2026-03-21) Locate all wizard and action gating points:
rg -n "billingMethods=|must be a .*billing service|billing_method" packages/billing/src/components/billing-dashboard/contracts packages/billing/src/actions
- (2026-03-21) Locate billing engine null-fallback clauses:
rg -n "contract_line_id IS NULL|usage_tracking\\.contract_line_id.*IS NULL" packages/billing/src/lib/billing/billingEngine.ts
- (2026-03-21) Locate downstream schema/interface coupling:
rg -n "billing_method|service_type" server/src/lib/api/schemas server/src/interfaces packages/types/src/interfaces packages/scheduling/src/actions packages/client-portal/src/services
- (2026-03-21) Validate plan artifacts:
jq empty ee/docs/plans/2026-03-21-service-catalog-billing-mode-decoupling/features.jsonjq empty ee/docs/plans/2026-03-21-service-catalog-billing-mode-decoupling/tests.jsonpython3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/validate_plan.py ee/docs/plans/2026-03-21-service-catalog-billing-mode-decoupling
- (2026-03-21) Validate canonicalization changes and migration guard:
cd server && npx vitest run src/test/unit/migrations/billingMethodCanonicalizationMigration.test.ts src/test/unit/api/contractLineCadenceOwner.schema.test.ts src/test/unit/api/defaultBillingSettings.cadenceOwner.schema.test.ts
- (2026-03-21) Validate API rejection + mode-default table migration tests:
cd server && npx vitest run src/test/unit/api/serviceBillingMethodCutover.schema.test.ts src/test/unit/migrations/serviceCatalogModeDefaultsMigration.test.ts src/test/unit/migrations/billingMethodCanonicalizationMigration.test.ts
- (2026-03-21) Validate mode-default backfill migration tests:
cd server && npx vitest run src/test/unit/migrations/serviceCatalogModeDefaultsMigration.test.ts src/test/unit/migrations/serviceCatalogModeDefaultsBackfillMigration.test.ts
- (2026-03-21) Validate wizard picker decoupling static guards:
cd server && npx vitest run src/test/unit/fixedWizardPickerPolicy.static.test.ts
- (2026-03-21) Validate server submit decoupling integration tests:
cd server && npx vitest run src/test/integration/contractWizard.integration.test.ts- Note: blocked locally in this run due unavailable DB test harness (
ECONNREFUSED 127.0.0.1:5438).
- (2026-03-21) Validate contract-line service attach mode-context guards:
cd server && npx vitest run src/test/unit/api/contractLineService.decoupledAttach.static.test.ts
Links / References
- Plan folder:
ee/docs/plans/2026-03-21-service-catalog-billing-mode-decoupling/ - Prior related plans:
ee/docs/plans/2026-03-18-service-driven-invoicing-cutover/ee/docs/plans/2026-03-20-grouped-automatic-invoices-selection/ee/docs/plans/2026-03-20-multi-active-contracts-per-client/
- Key files:
packages/billing/src/actions/contractWizardActions.tspackages/billing/src/actions/contractLineServiceActions.tspackages/billing/src/lib/billing/billingEngine.tspackages/billing/src/actions/serviceActions.tsserver/src/lib/api/services/ServiceCatalogService.tsserver/src/lib/api/services/ProductCatalogService.tspackages/scheduling/src/actions/timeEntryCrudActions.tspackages/client-portal/src/services/availabilityService.tsserver/src/lib/api/schemas/serviceSchemas.tsserver/src/test/e2e/utils/e2eTestSetup.ts
Open Questions
-
None blocking. Hard cutover policy is set; remaining implementation work is execution order only.
-
(2026-03-21) Completed F012-F014 by resolving fixed/hourly/usage wizard prefill from
service_catalog_mode_defaultskeyed by line mode + currency, with fallback to catalogdefault_rateand explicit submission overrides taking precedence. Key ref:packages/billing/src/actions/contractWizardActions.ts. -
(2026-03-21) Hardened wizard currency validation to treat mode-default/catalog-default resolved rates as priced inputs so contracts do not fail when no
service_pricesrow exists but valid defaults do. Rationale: preserve prefill semantics under decoupled model. -
(2026-03-21) Replaced temporary static checks with DB-backed integration tests
T021-T024inserver/src/test/integration/contractWizard.integration.test.tscovering mode-default prefill and override precedence for fixed/hourly/usage. -
(2026-03-21) Test run:
cd server && npx vitest run src/test/integration/contractWizard.integration.test.tscurrently blocked locally by Postgres not listening on127.0.0.1:5438/::1:5438; suite collected and skipped tests due beforeAll connection failure. -
(2026-03-21) Completed F015 by adding contract-line form metadata labels (
Effective mode,Default source) and switching mode derivation to contract-line context instead ofservice_catalog.billing_method. Key refs:ContractLineServiceForm.tsx,ServiceConfigurationPanel.tsx,BaseServiceConfigPanel.tsx. -
(2026-03-21) Added
T025UI test coverage inpackages/billing/tests/contractLineServiceForm.metadata.test.tsxvalidating catalog-default, contract-override, and none source labels plus contract-line effective mode mapping. -
(2026-03-21) Test run:
cd server && npx vitest run ../packages/billing/tests/contractLineServiceForm.metadata.test.tsx(pass). -
(2026-03-21) Completed F016/F017 by updating draft/template resume readers to preload
service_catalog_mode_defaults(hourly/usage) and prefer mode-default rates before catalog fallback when reconstructing wizard snapshots. Key ref:getDraftContractForResume,getContractTemplateSnapshotForClientWizardincontractWizardActions.ts. -
(2026-03-21) Added/updated
T026/T027inpackages/billing/tests/draftContractForResumeActions.test.tsto assert resume/template round-trip uses decoupled selections and mode-default prefills when stored rates are empty. -
(2026-03-21) Test run:
cd server && npx vitest run ../packages/billing/tests/draftContractForResumeActions.test.ts(pass). -
(2026-03-21) Completed F018-F022 in billing engine by removing unconditional null-line allocation and introducing service-constrained allocation: explicit
contract_line_idfirst, otherwise unassigned rows only when service-to-line match is unique for active client assignments in the service window. Added contract-line service membership filters for both time and usage. -
(2026-03-21) Completed F023-F025 guard rails by preserving existing hourly minimum/round/overtime logic, usage minimum/custom-rate/tier logic, and bucket overage path; added static assertions to lock these behaviors.
-
(2026-03-21) Added/updated tests:
server/src/test/unit/billingEngine.test.ts(T030-T035 behavior coverage) andserver/src/test/unit/billing/billingEngine.contractLineAllocation.static.test.ts(T028-T029, T036-T040 static guards). -
(2026-03-21) Test run:
cd server && npx vitest run src/test/unit/billingEngine.test.ts src/test/unit/billing/billingEngine.contractLineAllocation.static.test.ts(pass). -
(2026-03-21) Completed F026/F027 by extending due-work materialization to append unresolved non-contract rows (time + usage) using deterministic
schedule:...:non_contract:<type>:<recordId>selectors and client-level currency/tax metadata ingetAvailableRecurringDueWork. Key refs:packages/billing/src/actions/billingAndTax.ts,packages/billing/src/lib/billing/billingEngine.ts. -
(2026-03-21) Completed F028/F029/F030 by rendering non-contract child labels in grouped Automatic Invoices UI and preserving child-level generation targeting so contract-only and non-contract-only selection paths generate independently. Key ref:
packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx. -
(2026-03-21) Added non-contract selection handling in invoice generation scoping: non-contract selector keys bypass recurring obligation scoping and flow through dedicated
nonContractSelectionoptions. Key ref:packages/billing/src/actions/invoiceGeneration.ts. -
(2026-03-21) Added tests for T041-T045:
server/src/test/unit/billing/nonContractDueWork.integration.test.tsserver/src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsx
-
(2026-03-21) Updated preview summary copy to explicitly differentiate one combined invoice vs multiple separate invoices in grouped preview dialog (
AutomaticInvoices.tsx). -
(2026-03-21) Completed F031 by extending grouped UI combinability checks to include invoice-window scope alongside client/currency/tax/export/PO and by validating mixed selection behavior end-to-end in UI tests. Key ref:
AutomaticInvoices.tsx. -
(2026-03-21) Marked F032 complete after verifying grouped preview summary explicitly states one combined invoice vs N separate invoices (
preview-invoice-count-summary), with test coverage inautomaticInvoices.nonContractSelection.ui.test.tsx. -
(2026-03-21) Added/updated
T011inserver/src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsxto assert compatible mixed selections combine as a parent-group target while incompatible scope selections (currency mismatch) split into child-level targets. -
(2026-03-21) Marked
T001complete by validating migration canonicalization + mode-default schema/backfill suites (billingMethodCanonicalizationMigration,serviceCatalogModeDefaultsMigration,serviceCatalogModeDefaultsBackfillMigration). -
(2026-03-21) Completed F033 by decoupling service-catalog API filter/sort semantics from billing metadata for non-service contexts:
billing_methodfiltering and sort precedence now apply only whenitem_kind='service', with safe service-name fallback elsewhere. -
(2026-03-21) Completed F034 by mirroring the same service-only billing-metadata filter/sort semantics in shared helper
shared/billingClients/services.tsto keep caller behavior consistent across API and shared query paths. -
(2026-03-21) Added static guard coverage
T014inserver/src/test/unit/billing/serviceQueryDecoupling.static.test.tsto lock decoupled filter/sort behavior in bothServiceCatalogServiceand shared billing-client helpers. -
(2026-03-21) Marked
T002complete via backfill migration guard assertions inserviceCatalogModeDefaultsBackfillMigration.test.ts(unmappable mode + missing required default mappings fail fast with actionable messages). -
(2026-03-21) Completed F035 by removing scheduling lookup proxying (
sc.billing_method as service_type) intimeEntryCrudActions; service identity now resolves fromservice_types.name, while billing metadata is returned separately asbilling_mode+item_kind. -
(2026-03-21) Completed F036 by updating client-portal availability service queries to join
service_typesand exposeservice_typefrom taxonomy instead of aliasingbilling_method;billing_modeanditem_kindare returned as metadata fields. -
(2026-03-21) Added static guard coverage
T015inserver/src/test/unit/billing/schedulingAvailabilityDecoupling.static.test.tsto prevent regression tobilling_method as service_typeproxying in scheduling and client-portal availability reads. -
(2026-03-21) Completed F037 by canonicalizing product catalog writes in
ProductCatalogServicecreate/update paths from legacyper_unittousageso active product writes no longer emit legacy billing vocabulary. -
(2026-03-21) Added static write guard
server/src/test/unit/api/productCatalogCanonicalWrites.static.test.tsto enforce canonical product billing writes and prevent reintroduction ofbilling_method: 'per_unit'in product service APIs. -
(2026-03-21) Completed F038/F039 by hard-cutting service/product schema contracts to canonical vocabulary: product schema now accepts/defaults
usage(notper_unit), and schema tests explicitly reject legacy values. Key refs:server/src/lib/api/schemas/productSchemas.ts,server/src/test/unit/api/productBillingMethodCutover.schema.test.ts. -
(2026-03-21) Completed F040 by removing
per_unitfrom server/shared billing interface unions and adding static guards that financial + contract-line schemas remain canonicalfixed|hourly|usage. Key refs:server/src/interfaces/billing.interfaces.ts,packages/types/src/interfaces/billing.interfaces.ts,server/src/test/unit/api/billingInterfacesCutover.static.test.ts. -
(2026-03-21) Marked T012/T013 complete based on schema + interface guard suite run (service/product cutover schemas and financial/contract-line canonical enums).
-
(2026-03-21) Completed F041 by decoupling onboarding service creation from service-type billing metadata: onboarding now captures an explicit
serviceBillingModeandsetupBillingwritesservice_catalog.billing_methodfrom onboarding input (fallbackusage) instead ofservice_types.billing_method. Key refs:packages/onboarding/src/actions/onboarding-actions/onboardingActions.ts,packages/onboarding/src/components/steps/BillingSetupStep.tsx,packages/onboarding/src/components/OnboardingWizard.tsx,packages/types/src/lib/onboardingWizard.ts. -
(2026-03-21) Completed F042 by removing service-type-driven billing auto-overwrite in service settings forms (
ServiceForm,QuickAddService); changing service type now updates taxonomy only and preserves user-selected billing mode. -
(2026-03-21) Added
T016guard coverage inserver/src/test/unit/billing/onboardingServiceTypeDecoupling.static.test.tsto enforce onboarding/service-settings decoupling and prevent regressions to identity-level billing coupling. -
(2026-03-21) Completed F043 by removing usage-tracking picker gating on
service_catalog.billing_method='usage'; usage tracking now sources picker options from service identity (item_kind !== 'product') via a shared memoized options set used in both filter + create/edit dialog. -
(2026-03-21) Added
T017guard coverage inserver/src/test/unit/billing/usageTrackingPickerDecoupling.static.test.tsto prevent reintroduction of billing-method-only filtering in usage tracking service pickers. -
(2026-03-21) Completed F044 debt purge by removing remaining catalog-method eligibility gates in contract-line authoring dialogs (
ContractLineDialog,CreateCustomContractLineDialog), deleting template-attach fixed-method coupling incontractLineServiceActions, and eliminating activeper_unitcompatibility references from billing UI/models/action types plus e2e constraint normalization. -
(2026-03-21) Added hard-cutover guard suite
server/src/test/unit/billing/hardCutoverDebtGuard.static.test.ts(T018) to assert noper_unitcompatibility in active billing paths, no pickerbillingMethodsgates in contract authoring, no unconditionalcontract_line_id IS NULLfallback SQL in billing engine, and nobilling_method as service_typealias proxying. -
(2026-03-21) Added
server/src/test/unit/billing/contractAuthoringDecouplingChecklist.static.test.tsto lock T003-T006 checklist behaviors (service-identity validation in wizard flows, template/draft resume decoupled mode-default hooks, contract-line attach mode validation, and override/default precedence hooks). -
(2026-03-21) Marked T007-T010 complete from passing billing engine + automatic invoice suites:
billingEngine.contractLineAllocation.static.test.tsbillingEngine.test.tsautomaticInvoices.nonContractSelection.ui.test.tsx
-
(2026-03-21) Validation runbooks:
cd server && npx vitest run src/test/unit/billing/nonContractDueWork.integration.test.ts src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsxcd server && npx vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.tsnpx tsc -p packages/billing/tsconfig.json --noEmit(fails on pre-existing unrelated package type errors; no new errors from the F026-F030 patch set).cd server && npx vitest run src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsx(pass, includes T011 and T049 assertions)cd server && npx vitest run src/test/unit/migrations/billingMethodCanonicalizationMigration.test.ts src/test/unit/migrations/serviceCatalogModeDefaultsMigration.test.ts src/test/unit/migrations/serviceCatalogModeDefaultsBackfillMigration.test.ts(pass)cd server && npx vitest run src/test/unit/billing/serviceQueryDecoupling.static.test.ts(pass, T014 guards)cd server && npx vitest run src/test/unit/billing/schedulingAvailabilityDecoupling.static.test.ts(pass, T015 guards)cd server && npx vitest run src/test/unit/api/productCatalogCanonicalWrites.static.test.ts(pass)cd server && npx vitest run src/test/unit/api/serviceBillingMethodCutover.schema.test.ts src/test/unit/api/productBillingMethodCutover.schema.test.ts src/test/unit/api/billingInterfacesCutover.static.test.ts(pass, T012/T013)cd server && npx vitest run src/test/unit/billing/onboardingServiceTypeDecoupling.static.test.ts(pass, T016)cd server && npx vitest run src/test/unit/billing/usageTrackingPickerDecoupling.static.test.ts(pass, T017)cd server && npx vitest run src/test/unit/billingEngine.test.ts src/test/unit/billing/billingEngine.contractLineAllocation.static.test.ts src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsx(pass, T007-T010)cd server && npx vitest run src/test/unit/billing/contractAuthoringDecouplingChecklist.static.test.ts src/test/unit/billing/hardCutoverDebtGuard.static.test.ts(pass, T003-T006 and T018)