PSA/ee/docs/plans/multi_currency_design.md
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

9.0 KiB

Multi-Currency Design & Implementation Plan

1. Executive Summary

This document details the technical implementation for "Native Currency Contracts" in the Alga PSA billing system. Core Principle: A contract is defined in a specific currency. All rates (fixed, hourly, usage) within that contract are treated as cents/sub-units of that currency. Invoices generated from the contract inherit that currency. We do not support real-time FX conversion or mixed-currency invoices in Phase 1.

2. Database Schema & Migrations

We will build upon the initial migration strategy to ensure data integrity across billing, clients, and taxation.

2.1 Core Tables

  • contracts: Add currency_code (CHAR(3), Not Null, Default 'USD').
    • Rationale: Defines the currency for all lines in this contract.
  • contract_templates: Add currency_code (CHAR(3), Not Null, Default 'USD').
    • Rationale: Templates act as blueprints; cloning a template copies this currency to the new contract.
  • clients: Add default_currency_code (CHAR(3), Not Null, Default 'USD').
    • Rationale: Defaults for manual invoices and new contract creation.
  • invoices: Add currency_code (CHAR(3), Not Null, Default 'USD').
    • Rationale: Persisted at generation time to ensure historical accuracy.

2.2 Taxation Safety (New)

  • tax_rates: Add currency_code (CHAR(3), Nullable).
    • Rationale: Critical for threshold-based taxes. If a tax rate has a "max tax amount" or "min threshold" defined in absolute numbers (e.g., 5000), we must ensure it is only applied to invoices in the matching currency.
    • Behavior: If null, the rate is considered "currency agnostic" (pure percentage). If set, it only applies if the invoice currency matches.

2.3 Migration Script (20251118134500_add_multi_currency_support.cjs)

  • Update to include the tax_rates column.
  • Backfill Strategy: Update all existing rows to 'USD' to maintain current behavior.

3. Data Structures (TypeScript Interfaces)

3.1 Contract Interfaces (server/src/interfaces/contract.interfaces.ts)

export interface IContract {
  // ... existing fields
  currency_code: string; // e.g., 'USD', 'EUR', 'GBP'
}

export interface IContractDTO {
   // ... used for creation
   currency_code?: string;
}

3.2 Client Interfaces (server/src/interfaces/client.interfaces.ts)

export interface IClient {
  // ... existing fields
  default_currency_code: string;
}

3.3 Billing Interfaces (server/src/interfaces/billing.interfaces.ts)

export interface IBillingResult extends TenantEntity {
  // ... existing fields
  currency_code: string; // Propagated from contract(s)
}

export interface IBillingCharge {
  // ... existing fields
  // No currency code needed per-charge as they must align with the parent result
}

3.4 Invoice Interfaces (server/src/interfaces/invoice.interfaces.ts)

export interface IInvoice {
  // ... existing fields
  currency_code: string;
}

export interface InvoiceViewModel {
  // ... existing fields
  currencyCode: string; // Passed to Wasm template for formatting
}

4. Algorithm & Logic Updates

4.1 Billing Engine (server/src/lib/billing/billingEngine.ts)

The calculateBilling method requires a rigorous validation step before summing charges.

Revised Flow:

  1. Fetch Client Config: Retrieve client.default_currency_code.
  2. Fetch Contracts: When retrieving client_contract_lines and joining contracts, select contracts.currency_code.
  3. Validation (Critical Gap):
    • Group active contract lines by currency_code.
    • Rule: If multiple currencies are detected in the active contracts for the same billing cycle, throw a blocking error: "Billing Error: Client {id} has active contracts in multiple currencies ({currencies}). Mixed currency billing is not supported."
  4. Resolution:
    • If contracts exist, set billingResult.currency_code = contracts[0].currency_code.
    • If no contracts exist (e.g., usage-only billing or manual-only flow), default to client.default_currency_code.
  5. Charge Calculation: Proceed as normal. The system assumes all rate and total integers are in the resolved currency.

4.2 Invoice Service (server/src/lib/services/invoiceService.ts)

persistInvoiceCharges / createInvoice:

  • Accept currency_code as a required parameter or extract it from IBillingResult.
  • Insert into invoices table: currency_code.

updateInvoiceTotalsAndRecordTransaction:

  • No math changes needed (summing integers).
  • Ensure Transaction records inherit the currency if the transaction table supports it (or assume ledger matches invoice currency).

4.3 Manual Invoices (server/src/lib/actions/manualInvoiceActions.ts)

generateManualInvoice:

  1. Accept optional currency_code in input DTO.
  2. If provided, use it.
  3. If not provided, fetch client.default_currency_code and use it.
  4. Persist to invoices table.

4.4 Tax Service (server/src/lib/services/taxService.ts)

calculateTax:

  • Add currencyCode parameter.
  • Threshold Check: When loading tax_rates or tax_rate_thresholds:
    • If tax_rates.currency_code is NOT NULL and does NOT match input currencyCode, skip this rate or throw configuration error.
    • This prevents a "1000 JPY" threshold from acting like "1000 USD".

4.5 Formatting & Display (Frontend/Templates)

  • server/src/lib/i18n/client/index.ts: Ensure formatCurrency(amount, currencyCode) is used everywhere instead of hardcoded '$'.
  • Wasm Templates: Pass currencyCode to the template context. Update AssemblyScript logic to use Intl (if supported) or a map of symbols (USD -> $, EUR -> ) based on the code.

5. Implementation Roadmap

Phase 1: Database & Models

  1. Modify Migration: Add currency_code to tax_rates in 20251118134500....
  2. Run Migration: Apply changes locally.
  3. Update Models/Types: Update IContract, IClient, IInvoice definitions.

Phase 2: Logic Core

  1. Refactor BillingEngine: Implement the mixed-currency check and currency resolution logic.
  2. Update InvoiceService: Ensure currency is persisted.
  3. Update Contract Actions: Allow setting currency during creation/update.
  4. Update Manual Invoices: Add default currency lookup.

Phase 3: Safety & UI

  1. Tax Service Update: Implement currency-aware rate filtering.
  2. UI Updates: Add currency dropdowns to Contract forms and Client settings.
  3. Invoice Template: Pass currency to renderer.

6. Verification Scenarios

Scenario Setup Expected Outcome
Standard USD Client (USD), Contract (USD) Invoice created in USD.
Euro Contract Client (USD), Contract (EUR) Invoice created in EUR. Rates treated as Euro-cents.
Mixed Error Client (USD), Contract A (USD), Contract B (EUR) active same cycle Billing Fails with explicit error message.
Manual Invoice Client (GBP), No Contract Invoice created in GBP (client default).
Tax Mismatch Invoice (EUR), Tax Rate (Defined as USD-only) Tax Service ignores USD rate or throws config error (depending on strictness setting).

7. Phased Todo List

Phase 1: Schema & Types

  • Update migration '20251118134500_add_multi_currency_support.cjs' to include 'tax_rates.currency_code' for tax safety (See Plan 2.2).
  • Apply EE migrations to update the database.
  • Update 'contract.interfaces.ts' and 'contractTemplate.interfaces.ts' with 'currency_code'.
  • Update 'client.interfaces.ts' (default_currency) and 'invoice.interfaces.ts' (currency_code).
  • Update 'billing.interfaces.ts' to include 'currency_code' in IBillingResult.

Phase 2: Logic Implementation

  • Update 'contractActions.ts' and 'contractTemplate' model to support saving/updating 'currency_code'.
  • Update 'BillingEngine.ts' to fetch contract currencies and validate consistency (throw error on mixed currencies) (See Plan 4.1).
  • Update 'BillingEngine.ts' to propagate the resolved 'currency_code' in IBillingResult.
  • Update 'invoiceService.ts' ('persistInvoiceCharges') to save 'currency_code' to the invoices table.
  • Update 'manualInvoiceActions.ts' to use input currency or default to client's 'default_currency_code'.
  • Update 'TaxService.ts' to accept 'currencyCode' and filter tax rates/thresholds to match the invoice currency (See Plan 4.4).

Phase 3: UI & Templates

  • Update Contract and Template forms to include a currency selector.
  • Update Client settings form to include 'Default Currency' selector.
  • Update 'wasm-executor.ts' to inject 'currencyCode' into the invoice template context.
  • Update Standard Templates (AssemblyScript) to use 'currencyCode' for formatting monetary values.

Phase 4: Verification

  • Perform manual test: Create EUR Contract -> Bill -> Verify Invoice Currency.
  • Perform manual test: Attempt mixed currency billing and verify blocking error.