Saltar al contenido principal

Exchange Rates Module

Overview

The exchange-rates module manages foreign exchange (FX) rates scoped to a business. Each rate maps a source currency to a target currency with a numeric multiplier and an optional validity window (validFrom / validTo).

This is used by multi-currency features (sales, accounts payable/receivable, invoicing) to convert amounts between currencies at the rate effective on a given date.

Architecture

Follows hexagonal architecture:

exchange-rates/
├── exchange-rates.module.ts # NestJS module
├── application/
│ └── exchange-rates.service.ts # Use cases
├── domain/
│ └── exchange-rates-repository.domain.ts # Repository port + domain types
├── infrastructure/
│ └── exchange-rates.repository.ts # Kysely adapter
└── interfaces/
├── exchange-rates.controller.ts # HTTP endpoints
├── dtos/
│ ├── create-exchange-rate.dto.ts
│ ├── update-exchange-rate.dto.ts
│ └── exchange-rate-with-currencies-response.dto.ts
└── query/
└── paginate-exchange-rates.query.ts

Dependency injection

The repository is injected via the EXCHANGE_RATES_REPOSITORY token, allowing the service to depend on the IExchangeRatesRepository interface rather than the concrete Kysely implementation.

Domain Concepts

ConceptDescription
ExchangeRateA business-scoped FX rate between two currencies
fromCurrencyIdSource currency (convert from)
toCurrencyIdTarget currency (convert to)
rateMultiplier: 1 unit of source = rate units of target
validFrom / validToEffectiveness window (validTo = null means no expiration)
isActiveSoft-delete flag

Enriched type

ExchangeRateWithCurrencies extends the base row with joined currency fields: fromCurrencyName, fromCurrencySymbol, fromCurrencyCountryIsoCodes, and the equivalent to* fields.

API Endpoints

All endpoints require authentication (Bearer token) and are protected by RolesGuard with PolicyResource.ExchangeRate.

MethodPathDescription
POST/exchange-ratesCreate a new exchange rate
GET/exchange-rates/business/:businessIdList active rates for a business (paginated, with currency details)
GET/exchange-rates/:idGet a single exchange rate by ID
PATCH/exchange-rates/:idPartially update an exchange rate
DELETE/exchange-rates/:idSoft-delete an exchange rate

Example: Create exchange rate

POST /exchange-rates
{
"businessId": "550e8400-e29b-41d4-a716-446655440000",
"createdBy": "550e8400-e29b-41d4-a716-446655440001",
"fromCurrencyId": "currency-uuid-usd",
"toCurrencyId": "currency-uuid-gtq",
"rate": 7.90,
"validFrom": "2026-01-01T00:00:00Z",
"validTo": null
}

Example: List response (business-scoped)

GET /exchange-rates/business/:businessId?size=10&page=1

{
"data": [
{
"id": "uuid",
"businessId": "uuid",
"rate": 7.90,
"validFrom": "2026-01-01T00:00:00.000Z",
"validTo": null,
"fromCurrencyName": "US Dollar",
"fromCurrencySymbol": "$",
"fromCurrencyCountryIsoCodes": "USD",
"toCurrencyName": "Guatemalan Quetzal",
"toCurrencySymbol": "Q",
"toCurrencyCountryIsoCodes": "GTQ"
}
],
"total": 1,
"page": 1,
"size": 10,
"totalPages": 1
}

Database

Table: exchange_rate

ColumnTypeNotes
iduuidPK, auto-generated
business_iduuidFK → business, cascade
is_activebooleanDefault true
created_attimestamptzAuto
created_byuuidFK → user
updated_attimestamptzNullable
updated_byuuidFK → user, set null
from_currency_iduuidFK → currency, cascade
to_currency_iduuidFK → currency, cascade
ratenumericNot null
valid_fromtimestamptzNot null
valid_totimestamptzNullable

Design Decisions

  1. Soft delete only — The API only exposes soft-delete (isActive = false). Hard delete is not available via HTTP.
  2. Business-scoped listing filters active rates — The GET /business/:businessId endpoint automatically filters isActive = true and joins currency details.
  3. No bidirectional rates — Each rate is unidirectional. To convert in the opposite direction, a separate rate record is needed (or the consumer computes 1/rate).