Skip to main content

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 IAddonPricesRepository port and sortableAddonPriceKeys. 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

ConceptDescription
addonPriceA price record linking an addon to a currency and a numeric amount
isDefaultMarks the canonical price for an addon (one per addon)
isActiveSoft-enables or disables a price record
validFrom / validUntilOptional ISO 8601 date-time bounds for time-limited pricing
reasonHuman-readable label for the price entry (e.g. "Promotional Q1")

Main Use Cases

Manual CRUD (via REST)

Use CaseMethodEndpoint
Create addon pricePOST/addon-prices
List all addon pricesGET/addon-prices
Get addon price by IDGET/addon-prices/:id
Update addon pricePATCH/addon-prices/:id
Delete addon priceDELETE/addon-prices/:id

Automated via Events

The service subscribes to three events emitted by the services module:

EventBehaviour
service.createIf the new service has an addonId, a default addon price is automatically created seeded from the service's price and currencyId
service.updateIf the service's addon association changes, a new addon price is created for the new addon
service.deleteAll 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:

ParamTypeDescription
pagenumberPage number (default: 1)
sizenumberPage size (0 = no limit)
orderBystringSort column (price, createdAt)
orderstringSort 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.

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.