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
| Concept | Description |
|---|---|
| Service | A sellable, non-inventory item (e.g., shipping, labor) |
| Category | Organizational grouping shared with products |
| Currency | Pricing currency (code + minor unit) |
| Addon | Optional linked addon entity |
| Taxes | JSON object with tax items (rate, type, code) |
Relations
Each service query returns ServiceWithRelations:
category— full category object (viajsonObjectFromsubquery)currency— full currency objectaddon— addon if linked, otherwisenull
Mixins: withCategoryInService, withCurrencyInService, withAddonInService in packages/backend/database/src/mixins/service.mixins.ts
API Endpoints
All endpoints require Bearer authentication. RBAC via PolicyResource.Service.
| Method | Path | Description | Permission |
|---|---|---|---|
POST | /services | Create a service | Create |
GET | /services | List services (paginated) | Read |
GET | /services/:id | Get service by ID | Read |
PATCH | /services/:id | Update a service | Update |
DELETE | /services/:id | Delete a service | Delete |
Query Parameters (GET /services)
| Param | Required | Description |
|---|---|---|
businessId | No | Scope to business |
categoryId | No | Filter by category |
search | No | Search name, description, code |
page | No | Page number (default: 1) |
size | No | Page size (default: 10) |
orderBy | No | Sort field: name, description, code |
order | No | Sort 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
| Event | Trigger | Payload |
|---|---|---|
service.create | After creation | ServiceWithRelations + optional addonId |
service.update | After update | ServiceWithRelations + optional addonId |
service.delete | After deletion | ServiceWithRelations |
Design Decisions
- DI token for repository —
SERVICES_REPOSITORYsymbol enables swapping implementations (e.g., for testing). - Relations via returning subqueries — Uses Kysely
jsonObjectFromin.returning()clauses instead of separate queries, keeping responses consistent across all repository methods. - Event-driven side effects — Mutations emit events (via
EventEmitter2) for downstream modules (e.g., addons) instead of direct coupling.