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
9.0 KiB
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: Addcurrency_code(CHAR(3), Not Null, Default 'USD').- Rationale: Defines the currency for all lines in this contract.
contract_templates: Addcurrency_code(CHAR(3), Not Null, Default 'USD').- Rationale: Templates act as blueprints; cloning a template copies this currency to the new contract.
clients: Adddefault_currency_code(CHAR(3), Not Null, Default 'USD').- Rationale: Defaults for manual invoices and new contract creation.
invoices: Addcurrency_code(CHAR(3), Not Null, Default 'USD').- Rationale: Persisted at generation time to ensure historical accuracy.
2.2 Taxation Safety (New)
tax_rates: Addcurrency_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_ratescolumn. - 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:
- Fetch Client Config: Retrieve
client.default_currency_code. - Fetch Contracts: When retrieving
client_contract_linesand joiningcontracts, selectcontracts.currency_code. - 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."
- Group active contract lines by
- 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.
- If contracts exist, set
- Charge Calculation: Proceed as normal. The system assumes all
rateandtotalintegers are in the resolved currency.
4.2 Invoice Service (server/src/lib/services/invoiceService.ts)
persistInvoiceCharges / createInvoice:
- Accept
currency_codeas a required parameter or extract it fromIBillingResult. - Insert into
invoicestable:currency_code.
updateInvoiceTotalsAndRecordTransaction:
- No math changes needed (summing integers).
- Ensure
Transactionrecords 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:
- Accept optional
currency_codein input DTO. - If provided, use it.
- If not provided, fetch
client.default_currency_codeand use it. - Persist to
invoicestable.
4.4 Tax Service (server/src/lib/services/taxService.ts)
calculateTax:
- Add
currencyCodeparameter. - Threshold Check: When loading
tax_ratesortax_rate_thresholds:- If
tax_rates.currency_codeis NOT NULL and does NOT match inputcurrencyCode, skip this rate or throw configuration error. - This prevents a "1000 JPY" threshold from acting like "1000 USD".
- If
4.5 Formatting & Display (Frontend/Templates)
server/src/lib/i18n/client/index.ts: EnsureformatCurrency(amount, currencyCode)is used everywhere instead of hardcoded '$'.- Wasm Templates: Pass
currencyCodeto the template context. Update AssemblyScript logic to useIntl(if supported) or a map of symbols (USD->$,EUR->€) based on the code.
5. Implementation Roadmap
Phase 1: Database & Models
- Modify Migration: Add
currency_codetotax_ratesin20251118134500.... - Run Migration: Apply changes locally.
- Update Models/Types: Update
IContract,IClient,IInvoicedefinitions.
Phase 2: Logic Core
- Refactor BillingEngine: Implement the mixed-currency check and currency resolution logic.
- Update InvoiceService: Ensure currency is persisted.
- Update Contract Actions: Allow setting currency during creation/update.
- Update Manual Invoices: Add default currency lookup.
Phase 3: Safety & UI
- Tax Service Update: Implement currency-aware rate filtering.
- UI Updates: Add currency dropdowns to Contract forms and Client settings.
- 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.