> ## Documentation 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 Rate

> ExchangeRate represents a daily FX conversion rate between two ISO 4217 currencies for a given date

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`.

| 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

```json theme={null}
{
  "data": {
    "type": "exchange_rate",
    "id": "c3e1b2f4-09d7-4a8e-b5e0-7f2a1d3c6890",
    "attributes": {
      "exchange_rate_id": "c3e1b2f4-09d7-4a8e-b5e0-7f2a1d3c6890",
      "source_currency": "USD",
      "target_currency": "EUR",
      "rate": "0.92150000",
      "rate_date": "2026-05-15",
      "source": "ecb-daily-feed",
      "created_at": "2026-05-15T08:00:14.000Z",
      "updated_at": "2026-05-15T08:00:14.000Z",
      "deleted_at": null
    },
    "relationships": {
      "workspace": {
        "data": {
          "type": "workspace",
          "id": "a1b2c3d4-1111-2222-3333-444455556666"
        }
      }
    }
  }
}
```

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