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

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_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
  • 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:<type>:<recordId> 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)