# 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_unit` is compatibility-only. Rationale: current migrations already moved toward `usage`, while product APIs still emit `per_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_unit` at 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 existing `service_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_prices` first, fallback to `service_catalog.default_rate` only 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) `addServiceToContractLine` now determines default configuration type from target `contract_line_type` (Fixed/Hourly/Usage) and validates explicit `configType` compatibility per line mode, instead of deriving from catalog/service-type billing method. ## Discoveries / Constraints - (2026-03-21) Wizard gates services by `billing_method` in picker and server submit validation. Key refs: `ServiceCatalogPicker`, `FixedFeeServicesStep`, `HourlyServicesStep`, `UsageBasedServicesStep`, `contractWizardActions`. - (2026-03-21) `contractLineServiceActions` still 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 NULL` in time/usage queries per contract-line pass, which can multi-claim unresolved rows without service-aware guard. - (2026-03-21) `time_entries` default to `contract_line_id = null` in save paths, so allocation correctness must be enforced centrally at billing execution. - (2026-03-21) Many downstream APIs and schemas require/expose `billing_method` as 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_unit` etc.), which can mask decoupled behavior regressions. - (2026-03-21) Existing migration `20251016120000_update_billing_method_to_text.cjs` normalized `per_unit` to `usage` but 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 in `F037/F039`. - (2026-03-21) Added migration `20260321110000_create_service_catalog_mode_defaults.cjs` with 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.cjs` that normalizes `per_unit -> usage`, inserts from `service_prices`, falls back to `service_catalog` defaults, and throws when active services with source defaults still have no mode-default rows. - (2026-03-21) `ServiceCatalogPicker` still supports optional `billing_methods`, but wizard and template steps no longer pass billing-method filters in fixed/hourly/usage service flows; only products step still uses `itemKinds=['product']` as expected. - (2026-03-21) Added integration coverage (`T013-T018`) in `server/src/test/integration/contractWizard.integration.test.ts` for decoupled acceptance (fixed/hourly/usage) and non-service rejections. - (2026-03-21) Removed `service_types` join and `service_type_billing_method` dependency in `contractLineServiceActions` attach 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.json` - `jq empty ee/docs/plans/2026-03-21-service-catalog-billing-mode-decoupling/tests.json` - `python3 /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.ts` - `packages/billing/src/actions/contractLineServiceActions.ts` - `packages/billing/src/lib/billing/billingEngine.ts` - `packages/billing/src/actions/serviceActions.ts` - `server/src/lib/api/services/ServiceCatalogService.ts` - `server/src/lib/api/services/ProductCatalogService.ts` - `packages/scheduling/src/actions/timeEntryCrudActions.ts` - `packages/client-portal/src/services/availabilityService.ts` - `server/src/lib/api/schemas/serviceSchemas.ts` - `server/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_defaults` keyed by line mode + currency, with fallback to catalog `default_rate` and 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_prices` row 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-T024` in `server/src/test/integration/contractWizard.integration.test.ts` covering 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.ts` currently blocked locally by Postgres not listening on `127.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 of `service_catalog.billing_method`. Key refs: `ContractLineServiceForm.tsx`, `ServiceConfigurationPanel.tsx`, `BaseServiceConfigPanel.tsx`. - (2026-03-21) Added `T025` UI test coverage in `packages/billing/tests/contractLineServiceForm.metadata.test.tsx` validating 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`, `getContractTemplateSnapshotForClientWizard` in `contractWizardActions.ts`. - (2026-03-21) Added/updated `T026/T027` in `packages/billing/tests/draftContractForResumeActions.test.ts` to 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_id` first, 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) and `server/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::` selectors and client-level currency/tax metadata in `getAvailableRecurringDueWork`. 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 `nonContractSelection` options. Key ref: `packages/billing/src/actions/invoiceGeneration.ts`. - (2026-03-21) Added tests for T041-T045: - `server/src/test/unit/billing/nonContractDueWork.integration.test.ts` - `server/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 in `automaticInvoices.nonContractSelection.ui.test.tsx`. - (2026-03-21) Added/updated `T011` in `server/src/test/unit/billing/automaticInvoices.nonContractSelection.ui.test.tsx` to 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 `T001` complete 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_method` filtering and sort precedence now apply only when `item_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.ts` to keep caller behavior consistent across API and shared query paths. - (2026-03-21) Added static guard coverage `T014` in `server/src/test/unit/billing/serviceQueryDecoupling.static.test.ts` to lock decoupled filter/sort behavior in both `ServiceCatalogService` and shared billing-client helpers. - (2026-03-21) Marked `T002` complete via backfill migration guard assertions in `serviceCatalogModeDefaultsBackfillMigration.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`) in `timeEntryCrudActions`; service identity now resolves from `service_types.name`, while billing metadata is returned separately as `billing_mode` + `item_kind`. - (2026-03-21) Completed F036 by updating client-portal availability service queries to join `service_types` and expose `service_type` from taxonomy instead of aliasing `billing_method`; `billing_mode` and `item_kind` are returned as metadata fields. - (2026-03-21) Added static guard coverage `T015` in `server/src/test/unit/billing/schedulingAvailabilityDecoupling.static.test.ts` to prevent regression to `billing_method as service_type` proxying in scheduling and client-portal availability reads. - (2026-03-21) Completed F037 by canonicalizing product catalog writes in `ProductCatalogService` create/update paths from legacy `per_unit` to `usage` so active product writes no longer emit legacy billing vocabulary. - (2026-03-21) Added static write guard `server/src/test/unit/api/productCatalogCanonicalWrites.static.test.ts` to enforce canonical product billing writes and prevent reintroduction of `billing_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` (not `per_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_unit` from server/shared billing interface unions and adding static guards that financial + contract-line schemas remain canonical `fixed|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 `serviceBillingMode` and `setupBilling` writes `service_catalog.billing_method` from onboarding input (fallback `usage`) instead of `service_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 `T016` guard coverage in `server/src/test/unit/billing/onboardingServiceTypeDecoupling.static.test.ts` to 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 `T017` guard coverage in `server/src/test/unit/billing/usageTrackingPickerDecoupling.static.test.ts` to 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 in `contractLineServiceActions`, and eliminating active `per_unit` compatibility 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 no `per_unit` compatibility in active billing paths, no picker `billingMethods` gates in contract authoring, no unconditional `contract_line_id IS NULL` fallback SQL in billing engine, and no `billing_method as service_type` alias proxying. - (2026-03-21) Added `server/src/test/unit/billing/contractAuthoringDecouplingChecklist.static.test.ts` to 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.ts` - `billingEngine.test.ts` - `automaticInvoices.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.tsx` - `cd server && npx vitest run src/test/unit/billing/recurringDueWorkReader.integration.test.ts` - `npx 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)