Skip to main content
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 the exchange_rates root and referenced as a relation from invoices and invoice_transactions.

API operations

Data model

Attributes

Relationships

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

Source: apps/api/src/database/entities/ExchangeRate.ts · domain: financial-graph · tier: Supporting