Addon Prices Module
Overview
The addon-prices module manages pricing records for service add-ons in FlowPOS. Each addonPrice record defines a monetary price for a specific addon in a specific currency, with optional validity windows and a flag for the default price.
The module exposes a REST API for manual CRUD operations and also listens to internal service lifecycle events to keep addon price records in sync automatically.
Architecture
This module follows the Hexagonal Architecture (ports & adapters) pattern used across the backend:
addon-prices/
├── addon-prices.module.ts # NestJS module wiring
├── application/
│ └── addon-prices.service.ts # Use cases + event handlers
├── domain/
│ └── addon-prices-repository.domain.ts # IAddonPricesRepository port + sortableAddonPriceKeys
├── infrastructure/
│ └── addon-prices.repository.ts # Kysely DB adapter
└── interfaces/
├── addon-prices.controller.ts # REST endpoints
├── dtos/
│ ├── create-addon-price.dto.ts
│ └── update-addon-price.dto.ts
└── query/
└── paginate-addon-prices.query.ts
Dependency flow
interfaces → application → domain ← infrastructure
- Domain owns the
IAddonPricesRepositoryport andsortableAddonPriceKeys. No framework imports. - Application (service) depends only on the domain types and the concrete repository (project convention).
- Infrastructure (repository) implements the domain port via Kysely queries.
- Interfaces (controller + DTOs) handles HTTP concerns and delegates to the service.
Domain Concepts
| Concept | Description |
|---|---|
addonPrice | A price record linking an addon to a currency and a numeric amount |
isDefault | Marks the canonical price for an addon (one per addon) |
isActive | Soft-enables or disables a price record |
validFrom / validUntil | Optional ISO 8601 date-time bounds for time-limited pricing |
reason | Human-readable label for the price entry (e.g. "Promotional Q1") |
Main Use Cases
Manual CRUD (via REST)
| Use Case | Method | Endpoint |
|---|---|---|
| Create addon price | POST | /addon-prices |
| List all addon prices | GET | /addon-prices |
| Get addon price by ID | GET | /addon-prices/:id |
| Update addon price | PATCH | /addon-prices/:id |
| Delete addon price | DELETE | /addon-prices/:id |
Automated via Events
The service subscribes to three events emitted by the services module:
| Event | Behaviour |
|---|---|
service.create | If the new service has an addonId, a default addon price is automatically created seeded from the service's price and currencyId |
service.update | If the service's addon association changes, a new addon price is created for the new addon |
service.delete | All addon prices belonging to the deleted service's addon are bulk-deleted |
API Endpoints
All endpoints require a valid Firebase ID token (Authorization: Bearer <token> or flowpos-id-token cookie) and the AddonPrice RBAC permission (@UseGuards(RolesGuard) + @PermissionResource(PolicyResource.AddonPrice)).
POST /addon-prices
Creates a new addon price record.
Required fields: price, addonId, currencyId, createdBy
Optional fields: isDefault, isActive, validFrom, validUntil, reason
{
"price": 25.99,
"addonId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currencyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isDefault": true,
"isActive": true,
"validFrom": "2026-01-01T00:00:00.000Z",
"validUntil": "2026-12-31T23:59:59.000Z",
"reason": "Standard rate"
}
Returns 201 Created with the created record.
GET /addon-prices
Returns a paginated list of all addon prices.
Query parameters:
| Param | Type | Description |
|---|---|---|
page | number | Page number (default: 1) |
size | number | Page size (0 = no limit) |
orderBy | string | Sort column (price, createdAt) |
order | string | Sort direction (asc, desc) |
GET /addon-prices/:id
Returns a single addon price by UUID. Returns 404 Not Found if the record does not exist.
PATCH /addon-prices/:id
Updates an existing addon price. All fields except updatedBy are optional.
Required fields: updatedBy
{
"price": 29.99,
"reason": "Updated rate for Q2 2026",
"updatedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Returns 200 OK with the updated record.
DELETE /addon-prices/:id
Deletes an addon price record. Returns 204 No Content on success.
Design Decisions
Sortable keys: price and createdAt
The sortableAddonPriceKeys tuple in the domain layer defines which columns can be used as sort keys via the orderBy query parameter. Currently price and createdAt are supported.
No free-text search
The search query parameter is accepted to maintain API compatibility but has no effect on the query — addonPrice has no text columns suitable for LIKE-style matching.
RBAC enforcement
The controller is protected by @UseGuards(RolesGuard) and @PermissionResource(PolicyResource.AddonPrice) at the class level, with per-method @PermissionAction decorators. This matches the standard pattern used across all resource controllers (e.g. brands, colors).
Automated price creation
When a service is linked to an addon via service.create, a default price is seeded from the service's own price and currency. This ensures every addon always has at least one price record without requiring a manual step.