Currencies Module
Overview
The currencies module manages the global currency catalog in FlowPOS. Each currency has a name, symbol, and ISO code. Currencies can be global (shared across all businesses, businessId = null) or business-scoped (owned by a specific business).
Currency assignment to a business is handled by the companion business-currencies module, which stores the link and tracks which currency is the default for each business.
Architecture
The module follows Hexagonal Architecture:
currencies/
├── currencies.module.ts
├── application/
│ ├── currencies.service.ts # Use cases
│ └── events/
│ ├── on-currency-create.event.ts # Emitted after create
│ └── on-currency-update.event.ts # Emitted after update
├── domain/
│ └── currencies-repository.domain.ts # Repository port + CurrencyWithBusinessRelation type
├── infrastructure/
│ └── currencies.repository.ts # Kysely implementation
└── interfaces/
├── currencies.controller.ts # HTTP routes
├── dtos/
│ ├── create-currency.dto.ts
│ └── update-currency.dto.ts
└── query/
└── paginate-currencies.query.ts
Layer responsibilities
| Layer | Responsibility |
|---|---|
| Domain | ICurrenciesRepository port, CurrencyWithBusinessRelation type, sortableCurrencyKeys, CurrencyData |
| Application | Orchestrates CRUD, emits/handles events. Depends only on domain port via CURRENCIES_REPOSITORY DI token |
| Infrastructure | Kysely queries against currency and businessCurrency tables. Owns transaction boundaries |
| Interface | HTTP routes, DTO validation, Swagger docs, RBAC (RolesGuard + PolicyResource.Currency) |
Dependency injection
The service depends on the ICurrenciesRepository port, not the concrete CurrenciesRepository. The binding is done in the module via:
{ provide: CURRENCIES_REPOSITORY, useClass: CurrenciesRepository }
This enables easy testing with mock repositories and enforces clean inward-pointing dependencies.
Domain Concepts
Currency (global vs business-scoped)
| Field | Description |
|---|---|
id | UUID primary key |
name | Full name, e.g. "Guatemalan Quetzal" |
symbol | Display symbol, e.g. "Q" |
countryIsoCodes | ISO 4217 code(s), e.g. "GTQ" |
businessId | null = global; UUID = business-owned |
isActive | Soft-delete flag |
currencyCode | Typed IsoCurrency enum |
minorUnit | CurrencyMinorUnit (decimal places) |
CurrencyWithBusinessRelation
Returned by the with-relation and unlinked-or-global endpoints. Extends SelectableCurrency with:
| Field | Description |
|---|---|
businessCurrencyId | UUID of the business_currency row, or null if not linked |
bcOrder | Ordering hint: 1 = linked, 2 = unlinked/global |
isDefault | Whether this currency is the business default |
Event-Driven Integration
The module uses NestJS EventEmitter2 to integrate across modules without direct dependencies.
| Event | Direction | Effect |
|---|---|---|
currency.create (OnCurrencyCreateEvent) | Emitted by CurrenciesService | BusinessCurrenciesService creates the business_currency link if businessId is present |
currency.update (OnCurrencyUpdateEvent) | Emitted by CurrenciesService | BusinessCurrenciesService updates the linked business_currency record |
businessCurrency.delete (OnDeleteBusinessCurrencyEvent) | Listened by CurrenciesService | Re-points all products using the deleted currency to the business default, then soft-deletes the currency |
Deletion Strategy
| Method | Behaviour |
|---|---|
delete() (repository) | Hard DELETE — only for global currencies with no business link |
deleteCurrency() (repository) | Soft delete: sets isActive = false on the currency row and all related business_currency rows |
deleteCurrencyWithProductReassignment() (repository) | Transactionally reassigns all products using the currency to the replacement, then soft-deletes. Used by the OnDeleteBusinessCurrencyEvent handler |
Always prefer deleteCurrency for business-scoped currencies. The hard delete is exposed on the domain interface for edge cases only. The deleteCurrencyWithProductReassignment method encapsulates the full transactional cleanup when a business-currency link is removed.
API Endpoints
Base path: /currencies
| Method | Path | Description |
|---|---|---|
POST | / | Create a currency |
GET | / | List all currencies (paginated, filterable) |
GET | /:id | Get currency by ID |
PATCH | /:id | Update currency |
DELETE | /:id | Soft-delete currency + business-currency links |
GET | /business/:businessId | List currencies owned by a specific business |
GET | /with-relation/:businessId | Currencies linked to business (with business-currency metadata) |
GET | /unlinked-or-global/:businessId | Currencies not yet linked to business, plus global ones |
All routes require Firebase Bearer authentication and Currency permission resource.
Query params for GET /
| Param | Type | Description |
|---|---|---|
search | string | ilike search on name, symbol, countryIsoCodes |
page | number | Page number (default: 1) |
size | number | Page size (default: 10; 0 = unlimited) |
orderBy | name | countryIsoCodes | symbol | Sort field |
order | asc | desc | Sort direction |
businessId | UUID | Filter to currencies owned by this business (pass null for globals) |
noRelatedToBusinessId | UUID | Exclude currencies already linked to this business |
Example Requests
Create a global currency
POST /currencies
Authorization: Bearer <token>
{
"name": "Guatemalan Quetzal",
"countryIsoCodes": "GTQ",
"symbol": "Q",
"isActive": true,
"createdBy": "<userId>"
}
Create a business-scoped currency and mark it as default
POST /currencies
Authorization: Bearer <token>
{
"name": "US Dollar",
"countryIsoCodes": "USD",
"symbol": "$",
"isActive": true,
"createdBy": "<userId>",
"businessId": "<businessId>",
"isDefault": true
}
List currencies not yet linked to a business
GET /currencies?noRelatedToBusinessId=<businessId>
Authorization: Bearer <token>
Get all currencies linked to a business (with relation metadata)
GET /currencies/with-relation/<businessId>
Authorization: Bearer <token>
Design Decisions
- Global vs business-scoped: Global currencies (
businessId = null) appear in all business dropdowns. Business-scoped currencies are private to that business. - Soft delete cascade: Deleting a currency also deactivates all its
business_currencylinks, preserving referential integrity without hard foreign-key cascades. noRelatedToBusinessIdfilter: Powers the "add currency to business" UI by showing only currencies the business hasn't linked yet.bcOrderfield: A computed hint (1= linked,2= unlinked) returned by join queries. Lets the frontend sort linked currencies first without a separate request.