Addons Module
Overview
The addons module manages the addon catalog for FlowPOS and orchestrates the installation and uninstallation of addons for businesses.
An addon is an optional, purchasable feature extension (e.g. Kitchen Display System, delivery platform integration) that a business can activate by subscribing to the linked service. The module supports a pluggable gateway pattern, enabling different checkout/cancellation providers (currently only local).
Architecture
This module follows the Hexagonal Architecture (ports & adapters) pattern:
addons/
├── addons.module.ts # NestJS module wiring
├── application/
│ ├── addons.service.ts # CRUD use cases + install/uninstall orchestration
│ └── addons-gateway.service.ts # Gateway provider factory
├── domain/
│ ├── addons-repository.domain.ts # IAddonsRepository port + ADDONS_REPOSITORY token
│ ├── addons-gateway.domain.ts # IAddonGatewayService port + ADDONS_GATEWAY_MAP token
│ └── addon-service.domain.ts # IInstallAddonParameters, IUninstallAddonParameters
├── infrastructure/
│ ├── addons.repository.ts # Kysely DB adapter
│ └── addons-local-gateway.service.ts # Local payment gateway adapter
└── interfaces/
├── addons.controller.ts # REST endpoints
├── dtos/
│ ├── create-addon.dto.ts
│ ├── update-addon.dto.ts
│ ├── install-addon.dto.ts
│ └── uninstall-addon.dto.ts
└── query/
└── paginate-addons.query.ts
Dependency flow
interfaces → application → domain ← infrastructure
- Domain owns the repository port (
IAddonsRepository+ADDONS_REPOSITORYinjection token), the gateway port (IAddonGatewayService+ADDONS_GATEWAY_MAPinjection token), and the use-case parameter interfaces. No framework imports. - Application orchestrates use cases.
AddonsServicedepends onIAddonsRepositoryvia injection token (not concrete class).AddonsGatewayServicereceives the gateway provider map viaADDONS_GATEWAY_MAPinjection token. - Infrastructure contains the Kysely repository adapter (
AddonsRepository) and the local gateway adapter (AddonsLocalGatewayService). Wired viauseClass/useFactoryin the module. - Interfaces handles HTTP routing, validation, and response shaping. Controller uses
@UseGuards(RolesGuard),@PermissionResource(PolicyResource.Addon), and@ApiBearerAuth().
Domain Concepts
| Concept | Description |
|---|---|
addon | A purchasable feature extension with a name, description, type, icon, and optional linked service |
addonType | Enum classifying the addon (BusinessAddon, LocationAddon) |
service | The billing service linked to the addon (from the services module) |
currentPrice | The active price for the addon (from the addon-prices module, resolved via a Kysely mixin) |
gateway | The payment/billing provider used for checkout and cancellation (local is the only current provider) |
associationAddon | The join record that tracks which business has which addon installed, including its subscription |
Main Use Cases
Manual CRUD (via REST)
| Use Case | Method | Endpoint |
|---|---|---|
| Create addon | POST | /addons |
| List all addons | GET | /addons |
| Get addon by ID | GET | /addons/:id |
| Update addon | PATCH | /addons/:id |
| Delete addon | DELETE | /addons/:id |
Installation workflow
| Use Case | Method | Endpoint |
|---|---|---|
| Install addon for a business | POST | /addons/install |
| Uninstall addon for a business | POST | /addons/uninstall |
Automated via Events
The service subscribes to two events emitted by the services module:
| Event | Behaviour |
|---|---|
service.create | If the new service has an addonId, the addon's serviceId is updated to link it |
service.update | If the service's addon association changes, the old addon is unlinked and the new one is linked (in a single transaction) |
API Endpoints
All endpoints require a valid Firebase ID token (Authorization: Bearer <token> or flowpos-id-token cookie) and the Addon RBAC permission.
POST /addons/install
Installs an addon for a business. Creates a subscription and an associationAddon record via the active gateway provider.
Required fields: businessId, createdBy, addonId, taxId, taxName, taxAddress
Optional fields: locationId
{
"businessId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"addonId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"taxId": "12345678-9",
"taxName": "Restaurante El Buen Sabor S.A.",
"taxAddress": "Avenida Reforma 123, Zona 10, Ciudad de Guatemala",
"locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Returns 201 Created with { "checkoutUrl": "" } (the local gateway returns an empty checkout URL; external gateways would return a redirect URL).
POST /addons/uninstall
Uninstalls an addon. Deletes the associationAddon record and cancels the linked subscription.
Required fields: businessId, associationAddonId
{
"businessId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"associationAddonId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Returns 200 OK.
POST /addons
Creates a new addon record.
Required fields: uniqueName, description, name, addonType
Optional fields: imagesUrl, iconUrl, serviceId, isActive, createdBy
{
"uniqueName": "restaurant-kds",
"description": "Kitchen Display System integration for restaurant orders",
"name": "Kitchen Display System",
"addonType": "BusinessAddon",
"isActive": true,
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Returns 201 Created.
GET /addons
Returns a paginated list of addons. Supports filtering by serviceId (pass null to find addons with no linked service) and addonType.
Query parameters:
| Param | Type | Description |
|---|---|---|
page | number | Page number (default: 1) |
size | number | Page size (0 = no limit) |
search | string | Full-text search on name, description, uniqueName |
serviceId | uuid | "null" | Filter by linked service (pass "null" to find unlinked addons) |
addonType | AddonType enum | Filter by addon type |
GET /addons/:id
Returns a single addon by UUID, including relations (tags, prices, currentPrice, linked service). Returns 404 Not Found if the record does not exist.
PATCH /addons/:id
Updates an existing addon. All fields except updatedBy are optional.
Required fields: updatedBy
DELETE /addons/:id
Deletes an addon. Returns 204 No Content on success.
Gateway Pattern
The install/uninstall flow is abstracted behind IAddonGatewayService:
interface IAddonGatewayService {
checkout(parameters: ICheckoutParameters): Promise<string>; // returns checkout URL
cancel(parameters: ICancelParameters): Promise<void>;
}
AddonsGatewayService is a factory that maps a gateway name string to the correct provider. It receives the provider map via the ADDONS_GATEWAY_MAP injection token (defined in the domain layer). The active gateway name is read from the ADDONS_PAYMENT_GATEWAY secret (falls back to "local").
To add a new gateway (e.g. Stripe):
- Create
infrastructure/addons-stripe-gateway.service.tsimplementingIAddonGatewayService - Register it as a provider in
addons.module.ts - Add it to the
ADDONS_GATEWAY_MAPfactory in the module'sprovidersarray
Design Decisions
sortableAddonKeys is empty
Defined in interfaces/query/paginate-addons.query.ts. The addon table has no natural user-facing sort columns beyond id. When a sortable column is needed, add it to the tuple.
Dependency injection via tokens
The repository and gateway map are injected via Symbol tokens (ADDONS_REPOSITORY, ADDONS_GATEWAY_MAP) rather than concrete classes. This keeps the application layer dependent only on domain interfaces, consistent with the brands, sizes, and addon-tags modules.
serviceId filter accepts "null" string
The query uses a @Transform decorator to convert the string "null" to JS null, allowing callers to query for addons with no linked service via a URL query parameter.
Install returns an empty checkout URL for the local gateway
The checkout() contract returns a URL string to support future external payment gateways (e.g. a Stripe checkout page). The local gateway completes the install synchronously and returns "". Clients should handle an empty URL as a successful local install.