Skip to main content

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

LayerResponsibility
DomainICurrenciesRepository port, CurrencyWithBusinessRelation type, sortableCurrencyKeys, CurrencyData
ApplicationOrchestrates CRUD, emits/handles events. Depends only on domain port via CURRENCIES_REPOSITORY DI token
InfrastructureKysely queries against currency and businessCurrency tables. Owns transaction boundaries
InterfaceHTTP 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)

FieldDescription
idUUID primary key
nameFull name, e.g. "Guatemalan Quetzal"
symbolDisplay symbol, e.g. "Q"
countryIsoCodesISO 4217 code(s), e.g. "GTQ"
businessIdnull = global; UUID = business-owned
isActiveSoft-delete flag
currencyCodeTyped IsoCurrency enum
minorUnitCurrencyMinorUnit (decimal places)

CurrencyWithBusinessRelation

Returned by the with-relation and unlinked-or-global endpoints. Extends SelectableCurrency with:

FieldDescription
businessCurrencyIdUUID of the business_currency row, or null if not linked
bcOrderOrdering hint: 1 = linked, 2 = unlinked/global
isDefaultWhether this currency is the business default

Event-Driven Integration

The module uses NestJS EventEmitter2 to integrate across modules without direct dependencies.

EventDirectionEffect
currency.create (OnCurrencyCreateEvent)Emitted by CurrenciesServiceBusinessCurrenciesService creates the business_currency link if businessId is present
currency.update (OnCurrencyUpdateEvent)Emitted by CurrenciesServiceBusinessCurrenciesService updates the linked business_currency record
businessCurrency.delete (OnDeleteBusinessCurrencyEvent)Listened by CurrenciesServiceRe-points all products using the deleted currency to the business default, then soft-deletes the currency

Deletion Strategy

MethodBehaviour
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

MethodPathDescription
POST/Create a currency
GET/List all currencies (paginated, filterable)
GET/:idGet currency by ID
PATCH/:idUpdate currency
DELETE/:idSoft-delete currency + business-currency links
GET/business/:businessIdList currencies owned by a specific business
GET/with-relation/:businessIdCurrencies linked to business (with business-currency metadata)
GET/unlinked-or-global/:businessIdCurrencies not yet linked to business, plus global ones

All routes require Firebase Bearer authentication and Currency permission resource.

Query params for GET /

ParamTypeDescription
searchstringilike search on name, symbol, countryIsoCodes
pagenumberPage number (default: 1)
sizenumberPage size (default: 10; 0 = unlimited)
orderByname | countryIsoCodes | symbolSort field
orderasc | descSort direction
businessIdUUIDFilter to currencies owned by this business (pass null for globals)
noRelatedToBusinessIdUUIDExclude 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_currency links, preserving referential integrity without hard foreign-key cascades.
  • noRelatedToBusinessId filter: Powers the "add currency to business" UI by showing only currencies the business hasn't linked yet.
  • bcOrder field: A computed hint (1 = linked, 2 = unlinked) returned by join queries. Lets the frontend sort linked currencies first without a separate request.