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
182 lines
9.0 KiB
Markdown
182 lines
9.0 KiB
Markdown
# 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`)
|
|
```typescript
|
|
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`)
|
|
```typescript
|
|
export interface IClient {
|
|
// ... existing fields
|
|
default_currency_code: string;
|
|
}
|
|
```
|
|
|
|
### 3.3 Billing Interfaces (`server/src/interfaces/billing.interfaces.ts`)
|
|
```typescript
|
|
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`)
|
|
```typescript
|
|
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
|
|
4. **Refactor BillingEngine**: Implement the mixed-currency check and currency resolution logic.
|
|
5. **Update InvoiceService**: Ensure currency is persisted.
|
|
6. **Update Contract Actions**: Allow setting currency during creation/update.
|
|
7. **Update Manual Invoices**: Add default currency lookup.
|
|
|
|
### Phase 3: Safety & UI
|
|
8. **Tax Service Update**: Implement currency-aware rate filtering.
|
|
9. **UI Updates**: Add currency dropdowns to Contract forms and Client settings.
|
|
10. **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.
|