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
17 KiB
17 KiB
Multi-Currency Billing Enablement Plan
Date: 2025-11-17
Author: Codex AI (billing pod)
Scope: All tenant-scoped billing, invoicing, ledger, reporting, and accounting export flows running in the monolith (server/*, shared/*, ee/*).
Executive Summary
- The public docs (
docs/overview.md,docs/billing.md) promise multi-currency billing, yet the live code base still hardcodes USD across pricing tables, UI components, invoice templates, and accounting exports. - Recent groundwork exists (e.g., migration
20251026120000_convert_invoice_and_transactions_currency.cjsaddsinvoices.currency_code+exchange_rate_basis_points,transactions.amountnow stores cents), but the data is never populated and upstream pricing sources (service_catalog, contract line pricing, manual invoices) remain currency-agnostic. - This plan delivers tenant-wide base currency configuration, per-client/contract currency overrides, deterministic FX snapshots per invoice, UI/template updates, reporting rollups, and adapter-safe exports while respecting existing rules (e.g., default invoice templates must flow through
invoice_template_assignmentsperdocs/AI_coding_standards.md).
Current State Assessment
Data & Persistence
- Money columns across
service_catalog,contract_lines,client_contract_line_pricing,client_contract_services,client_contract_line_terms,invoices,invoice_charges,credit_tracking, andtransactionsare integer cents with no associatedcurrency_code(seeserver/migrations/202409071803_initial_schema.cjs,20241125124900_add_credit_system.cjs). invoices.currency_code/exchange_rate_basis_pointsexist but are always NULL because neitherserver/src/lib/billing/billingEngine.tsnor manual invoice flows set them.clients(renamed from companies in20251003000001_company_to_client_migration.cjs) lack any currency preference columns;tenant_settings.settingshas locale data but no monetary configuration (seeserver/migrations/20250630161508_create_tenant_settings_table.cjs).- Accounting export tables (
accounting_export_batches,accounting_export_lines) expect acurrency_code(peree/docs/plans/2025-10-26-accounting-export-abstraction-plan.md) but ingest-only sees defaults from selectors.
Application Logic
BillingEngine.calculateBillingcomputes cents assuming tenant currency, with no awareness of client/contract currency or FX snapshots. Template cloning (server/src/lib/billing/utils/templateClone.ts) copiescustom_ratenumbers without currency context.- Manual invoicing (
server/src/lib/actions/manualInvoiceActions.ts,server/src/components/billing-dashboard/ManualInvoices.tsx) renders$and never stores acurrency_code. - Invoice persistence/services (
server/src/lib/services/invoiceService.ts,server/src/lib/models/invoice.ts,server/src/lib/actions/invoiceQueries.ts) neither read nor write currency metadata. - Finalization & ledger flows (
server/src/lib/actions/invoiceModification.ts,server/src/lib/actions/creditActions.ts,docs/invoice_finalization.md) make ledger entries in cents without knowing which currency they represent.
UI, Templates, and Client Portal
- Currency formatting is globally hard-coded to USD in
server/src/lib/utils/formatters.ts,server/src/lib/i18n/server.ts, and UI layers such asClientContractLineDashboard.tsx,contracts/ContractTemplateDetail.tsx,ManualInvoices.tsx,server/src/components/billing-dashboard/accounting/AccountingExportsTab.tsx, and client-portal surfaces (server/src/components/client-portal/billing/BillingOverview.tsx,client-portal/account/ClientAccount.tsx). - Invoice template Wasm helpers (
server/src/invoice-templates/assemblyscript/assembly/common/format-helpers.ts) prepend$and the hostWasmInvoiceViewModel(server/src/lib/invoice-renderer/types.ts) carries no currency metadata, so PDF generation (server/src/lib/actions/invoiceGeneration.ts) cannot localize values.
Reporting & Integrations
- Report definitions (
server/src/lib/reports/definitions/billing/overview.ts,contracts/*.ts) and the core engine (server/src/lib/reports/core/ReportEngine.ts) always emit USD, making KPI dashboards incorrect for non-USD tenants. - Accounting exports default to
'USD'(server/src/lib/services/accountingExportInvoiceSelector.ts,AccountingExportsTab.tsx) and only pass through invoice-level currency when available; adapters (server/src/lib/adapters/accounting/quickBooksOnlineAdapter.ts,.../xeroAdapter.ts) assume same-currency credits and ignore exchange-rate gaps noted in the accounting export plan.
Documentation Gap
docs/billing.md,docs/billing_cycles.md,docs/invoice_templates.md, anddocs/invoice_finalization.mdmake no mention of base currency resolution or FX handling even though marketing copy advertises multi-currency.
Goals & Non-Goals
- Goals: Tenant-level base currency selection, client/contract currency overrides, deterministic FX storage per invoice, UI/template formatting with ISO 4217 codes, ledger + credit handling in mixed currencies, reporting and export parity, strong migration/backfill + tests.
- Non-Goals: Implementing payment processor FX, exposing automated FX rate providers to end users, or redesigning invoice template assignment semantics (must continue via
invoice_template_assignments).
Guiding Principles
- Single Source of Truth: Base currency lives in
tenant_settings.settings.billing.currency. Client/contract overrides cascade deterministically (tenant → client → contract → invoice). - Fail Fast (per docs): Billing services throw when currency context is missing instead of guessing.
- Immutable FX Snapshots: Each invoice stores
currency_codeandexchange_rate_basis_pointstaken at generation time; charges inherit invoice currency. - Integer Math Everywhere: Continue storing cents; conversions happen via basis points to avoid floating precision issues.
- Backwards-Compatible APIs: External consumers keep receiving integers but now also get explicit
currency_codein DTOs.
Proposed Architecture
Currency Resolution Hierarchy
- Extend
tenant_settingsJSON to hold{ billing: { baseCurrency: 'USD', supportedCurrencies: [...] } }managed byserver/src/lib/actions/tenant-settings-actions/tenantSettingsActions.ts& locale actions. - Add
currency_codetoclients,client_contracts,client_contract_lines, andcontractsto capture overrides defined indocs/billing.md. Rates inservice_catalog,contract_lines, andclient_contract_line_pricinggaincurrency_codemetadata so cloning + billing knows how to convert before charge generation. BillingEnginedetermines the invoice currency per client billing cycle (seedocs/billing_cycles.md) and records it oninvoices+invoice_charges(explicit column or derived join) while persistingexchange_rate_basis_pointsrepresenting conversion to tenant base.
FX & Money Services
- Introduce
fx_ratestable storing(fx_rate_id, tenant, source_currency, target_currency, rate_basis_points, effective_at, provider, metadata)plus caching serviceserver/src/lib/services/exchangeRateService.ts. Provide admin override UI later if needed. - Add helper
Moneyutilities inshared/utilsandserver/src/lib/utils/money.tsto normalize conversions, rounding, formatting (tying intoserver/src/lib/i18n/server.ts).
Ledger & Reporting Alignment
- Transactions, credit tracking, and analytics events store both invoice currency (
currency_code) and converted base-cents (base_amount_cents). Reports roll up base amounts but also expose per-currency breakdown when filters require. - Accounting exports continue to emit the invoice currency while also including base currency + exchange rate for GL mapping.
Templates & Rendering
- Extend
WasmInvoiceViewModelwithcurrencyCode,currencySymbol, and localized helpers. Update AssemblyScript helpers to accept a currency code argument rather than hardcoding$, following the instructions indocs/invoice_templates.mdand ensuring default selection still respectsinvoice_template_assignments.
Implementation Plan
Phase 0 – Schema & Configuration Foundations (Week 1)
- Tenant Settings:
- Migration adding
settings->billingscaffold plus admin actions to read/write base currency + supported list (server/src/lib/actions/tenant-actions/tenantSettingsActions.ts,tenantLocaleActions.ts). - UI stub (likely under
server/src/components/settings/generalif needed) to set currency.
- Migration adding
- Entity Columns:
- Add
currency_codecolumns + composite indexes toclients,client_contracts,client_contract_lines,contracts,service_catalog,client_contract_line_pricing,client_contract_services, andinvoice_charges(even if it mirrors invoice currency for clarity) via new Knex migration. - Backfill existing rows: default to tenant base currency (fall back to
'USD'until admin sets it).
- Add
- Ledger Tables:
- Extend
transactions,credit_tracking, andclient_creditswithcurrency_code+ optionalbase_amount_centscolumns to prep for FX-aware credits described indocs/invoice_finalization.md.
- Extend
- Shared Types:
- Update
shared/interfaces/client.interfaces.ts,server/src/interfaces/*.ts(billing, invoice, contract, client) to include currency metadata. Ensure API controllers and Zod schemas (if any) accept the new fields.
- Update
Phase 1 – Billing & Invoice Generation Logic (Weeks 2-3)
- Currency Resolution Service:
- Build
resolveCurrencyForClient(clientId)that walks tenant → client → contract line overrides, returning{ currencyCode, exchangeRateBasisPoints }(usesExchangeRateService).
- Build
- Billing Engine Updates:
- Inject currency context into
BillingEngineso thatcalculateBillingpopulates invoice + charge currency fields, stores FX snapshot oninvoices, and ensures all downstream calculations stay integer (updateserver/src/lib/billing/billingEngine.tsand supporting repos/queries). - Update template cloning (
server/src/lib/billing/utils/templateClone.ts) and contract actions to persist rate currencies when copying from templates.
- Inject currency context into
- Manual + Automated Invoice Paths:
- Extend manual invoice actions (
manualInvoiceActions.ts,invoiceService.ts) to requirecurrency_code, persist it on the invoice row, and surface currency selection in the UI (ManualInvoices.tsx,LineItem.tsx). Respect component ID guidelines when adding new selects. - Update invoice generation pipeline (
server/src/lib/actions/invoiceGeneration.ts) so PDF/download flows include currency metadata inInvoiceViewModeland Wasm payloads.
- Extend manual invoice actions (
- Finalization & Credits:
- Modify
finalizeInvoice/unfinalizeInvoice+creditActions.tsto store ledger entries with both invoice and base currency amounts, handling FX gains/losses via the existingcurrency_adjustmenttransaction type.
- Modify
Phase 2 – UI, Templates, and Client Portal (Weeks 3-4)
- Shared Formatting Utilities:
- Replace ad-hoc USD formatting with a centralized
formatMoney(amountCents, currencyCode, locale)exported fromserver/src/lib/utils/formatters.tsandserver/src/lib/i18n/server.ts. Update consumers in billing dashboard cards, contract UIs,AccountingExportsTab.tsx, and report components.
- Replace ad-hoc USD formatting with a centralized
- Client Portal + Billing Dashboard:
- Propagate currency down to
BillingOverview.tsx,ContractLineDetailsDialog.tsx,client-portal/account/ClientAccount.tsx, and invoice tables underserver/src/components/billing-dashboard/invoicing/*.tsxso that drafts/finalized lists displaycurrency_codebadges and totals. - Ensure manual invoice UI, contract forms, and service configuration editors show the correct currency next to rate inputs (without renaming existing fields) and allow switching currency only where permissible (likely at client/contract level, not per line item).
- Propagate currency down to
- Invoice Templates & Renderer:
- Update
server/src/lib/invoice-renderer/types.ts,wasm-executor.ts, and AssemblyScript helpers to pass currency metadata to templates. Replace$informat-helpers.tswith dynamic symbol/resolved code. Update standard template sources underserver/src/invoice-templates/assemblyscript/standard/*and re-runsyncStandardTemplatesperdocs/invoice_templates.md. - Confirm
InvoiceTemplateEditor.tsxsurfaces currency references (maybe highlight preview currency based on selected invoice).
- Update
Phase 3 – Reporting, Analytics, and Integrations (Weeks 4-5)
- Reporting Layer:
- Teach
ReportEngine(server/src/lib/reports/core/ReportEngine.ts) to read currency metadata from metrics. Update billing + contract report definitions to either (a) convert to tenant base currency using stored exchange rates or (b) emit multi-series metrics grouped by currency. Updatedocs/billing.mdaccordingly.
- Teach
- Accounting Exports:
- Ensure selector (
server/src/lib/services/accountingExportInvoiceSelector.ts) pullsinvoices.currency_codeandexchange_rate_basis_points, defaulting only when absent. Update UI (AccountingExportsTab.tsx) to display multi-currency totals, and include FX data in batch detail drawers. - Update adapters for QuickBooks/Xero to handle currency mismatches, leveraging helper conversions and new repository fields (
server/src/lib/repositories/accountingExportRepository.ts).
- Ensure selector (
- API/Client Surfaces:
- Propagate currency fields through REST controllers (
server/src/lib/api/controllers/ApiAccountingExportController.ts,report-actions/getRecentClientInvoices.ts, etc.) and ensure client portal APIs (Next.js routes) include currency_code for invoice payloads.
- Propagate currency fields through REST controllers (
Phase 4 – Testing, Backfill, and Rollout (Week 6)
- Testing:
- Unit tests for money utilities, currency resolution, and FX snapshots.
- Integration tests: billing run with mixed currencies, invoice finalization applying credits across FX, accounting export batch containing multi-c invoices (
server/src/test/integration/accounting/*). - E2E/Playwright updates in
ee/server/src/__tests__/integration/batch-lifecycle.playwright.test.tsto cover UI filtering/rendering with multiple currencies.
- Backfill + Scripts:
- Write one-time script (e.g.,
scripts/backfill-invoice-currency.ts) to set currency_code/exchange_rate on existing invoices using tenant default + rate 1.0. - Provide validation script to compare totals between old/new representations (ensuring ledger balances).
- Write one-time script (e.g.,
- Rollout Controls:
- Feature flag (env or tenant_settings) gating UI display + FX enforcement until migrations complete.
- Monitoring: add PostHog analytics events enriched with
currency_codefor invoice generation/finalization so we can verify adoption.
Risks & Mitigations
- Historical Data Accuracy: Backfilling legacy invoices with assumed USD may not match actual currency; surface a per-tenant override tool and audit report to identify invoices where manual correction is needed.
- FX Source Reliability: Decide on provider (manual entry vs. API). Mitigate by allowing manual overrides and caching resolved rates alongside provider metadata.
- UI Complexity: Introducing currency selectors everywhere may overwhelm users; limit editing to tenant/client settings and surface read-only badges elsewhere.
- Downstream Integrations: QuickBooks/Xero realm currencies may reject mismatched codes; enforce validation in adapters before batch creation and raise actionable errors in
AccountingExportsTab.tsx.
Open Questions / Decisions Needed
- Do we support mixed currencies within a single tenant simultaneously, or enforce one invoice currency per client? (Plan assumes per-client/per-contract override but not per line item.)
- What is the authoritative exchange-rate provider (internal manual table vs. external API)?
- Should
transactions.amountremain invoice currency whilebase_amount_centsstores tenant currency, or should we flip (base inamount, invoice in extras)? - How should credit balances behave when applying a USD credit to a CAD invoice—immediate FX conversion or split ledgers?
Success Metrics
- 100% of new invoices carry non-null
currency_code+exchange_rate_basis_points. - Accounting export batches show accurate multi-currency totals and pass adapter validation for QuickBooks & Xero.
- Billing + contract reports display correct totals when at least two currencies exist in tenant data.
- Manual invoicing UI allows selecting/displaying non-USD currencies without regression failures.
Documentation & Follow-ups
- Update
docs/billing.md,docs/billing_cycles.md,docs/invoice_finalization.md, anddocs/invoice_templates.mdto document currency hierarchy and FX handling. - Document admin workflows in
docs/billing.md+ customer portal references. - Ensure release notes highlight required migrations and manual steps for self-hosted tenants.