Skip to main content

Services Module

Overview

The services module manages sellable non-product items in the FlowPOS catalog (e.g., shipping, consulting, installation). Each service belongs to a business, is assigned to a category, and has currency-based pricing with configurable tax rules.

Architecture

Follows hexagonal architecture (ports & adapters):

services/
├── services.module.ts # NestJS module with DI token wiring
├── application/
│ ├── services.service.ts # Use cases (create, list, update, delete)
│ └── events/ # Domain events emitted on mutations
├── domain/
│ ├── services-repository.domain.ts # Repository port (IServicesRepository) + DI token
│ └── services-service.domain.ts # Service-layer interfaces (ICreateService, IUpdateService)
├── infrastructure/
│ └── services.repository.ts # Kysely adapter implementing the port
└── interfaces/
├── services.controller.ts # HTTP controller with Swagger + RBAC
├── dtos/
│ ├── create-service.dto.ts
│ └── update-service.dto.ts
└── query/
└── paginate-services.query.ts

Dependency flow: Controller → Service → IServicesRepository (injected via SERVICES_REPOSITORY symbol) → ServicesRepository (Kysely)

Domain Concepts

ConceptDescription
ServiceA sellable, non-inventory item (e.g., shipping, labor)
CategoryOrganizational grouping shared with products
CurrencyPricing currency (code + minor unit)
AddonOptional linked addon entity
TaxesJSON object with tax items (rate, type, code)

Relations

Each service query returns ServiceWithRelations:

  • category — full category object (via jsonObjectFrom subquery)
  • currency — full currency object
  • addon — addon if linked, otherwise null

Mixins: withCategoryInService, withCurrencyInService, withAddonInService in packages/backend/database/src/mixins/service.mixins.ts

API Endpoints

All endpoints require Bearer authentication. RBAC via PolicyResource.Service.

MethodPathDescriptionPermission
POST/servicesCreate a serviceCreate
GET/servicesList services (paginated)Read
GET/services/:idGet service by IDRead
PATCH/services/:idUpdate a serviceUpdate
DELETE/services/:idDelete a serviceDelete

Query Parameters (GET /services)

ParamRequiredDescription
businessIdNoScope to business
categoryIdNoFilter by category
searchNoSearch name, description, code
pageNoPage number (default: 1)
sizeNoPage size (default: 10)
orderByNoSort field: name, description, code
orderNoSort direction: asc or desc

Create Service (POST /services)

{
"name": "Shipping",
"description": "Standard shipping of products",
"businessId": "<uuid>",
"isActive": true,
"createdBy": "<uuid>",
"categoryId": "<uuid>",
"currencyId": "<uuid>",
"currencyCode": "GTQ",
"minorUnit": 2,
"price": 8000,
"code": "SERV01",
"taxes": {
"items": [
{
"taxDefinitionId": "<uuid>",
"name": "IVA",
"code": "IVA",
"rate": 12.0,
"type": "percentage"
}
]
}
}

Update Service (PATCH /services/:id)

{
"businessId": "<uuid>",
"updatedBy": "<uuid>",
"description": "Updated description",
"price": 10000
}

Events

EventTriggerPayload
service.createAfter creationServiceWithRelations + optional addonId
service.updateAfter updateServiceWithRelations + optional addonId
service.deleteAfter deletionServiceWithRelations

Design Decisions

  1. DI token for repositorySERVICES_REPOSITORY symbol enables swapping implementations (e.g., for testing).
  2. Relations via returning subqueries — Uses Kysely jsonObjectFrom in .returning() clauses instead of separate queries, keeping responses consistent across all repository methods.
  3. Event-driven side effects — Mutations emit events (via EventEmitter2) for downstream modules (e.g., addons) instead of direct coupling.