Skip to main content

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_REPOSITORY injection token), the gateway port (IAddonGatewayService + ADDONS_GATEWAY_MAP injection token), and the use-case parameter interfaces. No framework imports.
  • Application orchestrates use cases. AddonsService depends on IAddonsRepository via injection token (not concrete class). AddonsGatewayService receives the gateway provider map via ADDONS_GATEWAY_MAP injection token.
  • Infrastructure contains the Kysely repository adapter (AddonsRepository) and the local gateway adapter (AddonsLocalGatewayService). Wired via useClass / useFactory in the module.
  • Interfaces handles HTTP routing, validation, and response shaping. Controller uses @UseGuards(RolesGuard), @PermissionResource(PolicyResource.Addon), and @ApiBearerAuth().

Domain Concepts

ConceptDescription
addonA purchasable feature extension with a name, description, type, icon, and optional linked service
addonTypeEnum classifying the addon (BusinessAddon, LocationAddon)
serviceThe billing service linked to the addon (from the services module)
currentPriceThe active price for the addon (from the addon-prices module, resolved via a Kysely mixin)
gatewayThe payment/billing provider used for checkout and cancellation (local is the only current provider)
associationAddonThe join record that tracks which business has which addon installed, including its subscription

Main Use Cases

Manual CRUD (via REST)

Use CaseMethodEndpoint
Create addonPOST/addons
List all addonsGET/addons
Get addon by IDGET/addons/:id
Update addonPATCH/addons/:id
Delete addonDELETE/addons/:id

Installation workflow

Use CaseMethodEndpoint
Install addon for a businessPOST/addons/install
Uninstall addon for a businessPOST/addons/uninstall

Automated via Events

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

EventBehaviour
service.createIf the new service has an addonId, the addon's serviceId is updated to link it
service.updateIf 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:

ParamTypeDescription
pagenumberPage number (default: 1)
sizenumberPage size (0 = no limit)
searchstringFull-text search on name, description, uniqueName
serviceIduuid | "null"Filter by linked service (pass "null" to find unlinked addons)
addonTypeAddonType enumFilter 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):

  1. Create infrastructure/addons-stripe-gateway.service.ts implementing IAddonGatewayService
  2. Register it as a provider in addons.module.ts
  3. Add it to the ADDONS_GATEWAY_MAP factory in the module's providers array

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.