ExchangeRate represents a daily FX conversion rate between two ISO 4217 currencies for a given date. It is used primarily by the multi-currency accounting pipeline: invoices and invoice-transactions reference an ExchangeRate row via a FK to convert document-currency amounts into the workspace’s accounting base currency. The workspace relation is nullable, allowing for global market-rate rows that are not scoped to a specific tenant, while workspace-specific overrides carry a workspace FK. It is a Supporting entity in the records graph, surfaced as theDocumentation Index
Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
Use this file to discover all available pages before exploring further.
exchange_rates root and referenced as a relation from invoices and invoice_transactions.
| Naming | Value |
|---|---|
| Object | Exchange Rate |
Resource type (JSON:API type) | exchange_rate |
| Collection / records root | exchange_rates |
| REST base | /v1/exchange-rates |
| Entity class | ExchangeRate |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/exchange-rates | ✅ Implemented |
| Retrieve | GET /v1/exchange-rates/{id} | ✅ Implemented |
| Create | POST /v1/exchange-rates | 🟡 Planned |
| Update | PATCH /v1/exchange-rates/{id} | 🟡 Planned |
| Delete | DELETE /v1/exchange-rates/{id} | 🟡 Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| exchange_rate_id | string, UUID | ✅ Yes | unique; default gen_random_uuid() | — | Public stable identifier for this exchange rate row. Generated server-side via gen_random_uuid(); never set by the client. |
| source_currency | string (CurrencyCodeEnum) | ✅ Yes | NOT NULL; must differ from target_currency (CHECK source_currency != target_currency); maps to Postgres native enum currency_code_enum | ISO 4217 codes: USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, BGN, … (full CurrencyCodeEnum, ~180+ values) | The currency being converted from. Together with target_currency and rate_date, forms part of the composite uniqueness key (scoped to workspace). |
| target_currency | string (CurrencyCodeEnum) | ✅ Yes | NOT NULL; must differ from source_currency (CHECK source_currency != target_currency); maps to Postgres native enum currency_code_enum | ISO 4217 codes: USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, BGN, … (full CurrencyCodeEnum, ~180+ values) | The currency being converted to. The rate expresses how many target_currency units equal one source_currency unit. |
| rate | string (decimal, 18,8 precision) | ⚪ No | nullable; DECIMAL(18,8); CHECK rate > 0 (enforced in migration DDL) | — | The conversion rate from source_currency to target_currency on rate_date. Stored as a decimal string to preserve full precision. A null value indicates the rate was recorded but the numeric value is not yet resolved. |
| rate_date | Date (date column) | ✅ Yes | NOT NULL; columnType date (date-only, no time component); part of the composite uniqueness key (workspace, source_currency, target_currency, rate_date) | — | The calendar date for which this rate is valid. One row per (workspace, source_currency, target_currency, date) tuple. |
| source | string | ⚪ No | nullable; length 100 | — | Free-text provenance label for the rate — e.g. the feed name, connector slug, or manual entry identifier. Used to distinguish ECB daily rates from connector-sourced or user-overridden rates. |
| created_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | set by MikroORM onCreate lifecycle hook; never writable after creation | — | Timestamp when the row was first persisted. Set server-side only. |
| updated_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | set by MikroORM onCreate and onUpdate lifecycle hooks | — | Timestamp of the last mutation to this row. Auto-maintained by the ORM. |
| deleted_at | string (ISO 8601 datetime) | null | ⚪ No | nullable; soft-delete sentinel; queries must filter deleted_at IS NULL | — | Soft-delete timestamp. Non-null means this rate has been logically removed. All standard queries must include the deleted_at IS NULL predicate. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| workspace | to-one (workspace) | ⚪ No (nullable) | The Workspace that owns this exchange rate row. Nullable: a null workspace_pk indicates a global market-rate record not scoped to a specific tenant. When set, combined with source_currency, target_currency, and rate_date forms the unique constraint. FK: exchange_rates.workspace_pk → workspaces.pk. Index: idx_exchange_rates_workspace_date on (workspace_pk, rate_date). |
System-computed
- exchange_rate_id is generated server-side via gen_random_uuid() as the Postgres column default, with a JavaScript fallback randomUUID() set at entity construction time in the MikroORM entity. Never supplied by the client.
- created_at is set by the MikroORM onCreate lifecycle hook (new Date()) and never subsequently updated.
- updated_at is set by both onCreate and onUpdate lifecycle hooks, reflecting the timestamp of the most recent mutation.
- deleted_at is null by default. Soft-delete is applied by setting this field to a non-null timestamp; hard deletes are not used. All read queries must filter deleted_at IS NULL.
- Composite uniqueness constraint: UNIQUE(workspace_pk, source_currency, target_currency, rate_date) — enforced at the database level. This makes the table an upsert target: the FX pipeline can safely attempt INSERT … ON CONFLICT (workspace_pk, source_currency, target_currency, rate_date) DO UPDATE.
- rate carries a database-level CHECK (rate > 0) constraint added in Migration20260306100000 DDL, even though the column is nullable — when rate is non-null, it must be strictly positive.
- The CHECK (source_currency != target_currency) constraint is enforced at the database level (Migration20260306100000). The MikroORM entity does not replicate this at the application layer; it is a DB-only guard.
- The workspace relation is nullable (nullable: true on the @ManyToOne decorator), allowing global rates to exist without a workspace owner. Workspace-scoped rates take precedence over global rates in the FX resolution service.
- Index idx_exchange_rates_workspace_date on (workspace_pk, rate_date) supports the FX rate lookup pattern: given a workspace and a date, resolve the applicable rate for a currency pair.
- Invoices reference ExchangeRate via invoices.exchange_rate_pk FK (added in Migration20260306100000). InvoiceTransaction rows reference ExchangeRate via invoice_transactions.exchange_rate_pk FK (added in Migration20260311100000_data_model_v2_accounting). ExchangeRate rows are pipeline-written; they are not created through the standard REST API by end users.
- The composites.yml file defines two composites for the exchange_rates root: composite_invoices_list (relation_list of linked invoices) and composite_invoice_transactions_list (relation_list of linked invoice_transactions). These are read-only aggregate views surfaced in the Records table.
- The workspace relation on exchange_rates is not guarded by the standard workspace-scoped Hasura RLS filter because the nullable workspace_pk means global rows have no workspace. This is an intentional architectural exception documented in the entity design.
Example
apps/api/src/database/entities/ExchangeRate.ts · domain: financial-graph · tier: Supporting