Service Bookings Module
Overview
The service-bookings module manages service booking records that link external booking platforms, service types, and multi-currency financial data. Each booking represents a scheduled service with participants, commission tracking, and optional linkage to retail sales.
All booking data is scoped to a businessId — no cross-business data access is possible.
Domain Concepts
| Concept | Description |
|---|---|
ServiceBooking | A record of a booked service with platform, type, participants, financials, and dates |
businessId | Multi-tenancy boundary. Every query filters by this field |
bookingPlatformId | References the bookingPlatform table (e.g., Airbnb, Booking.com) |
serviceTypeId | References the serviceType table |
externalRef | External reference identifier from the booking platform |
documentNumber | Auto-generated sequential document number |
detail | Flexible JSON structure for service item breakdown |
currency / customerPaidCurrency | Dual-currency support: booking currency and customer-paid currency |
commissionPercentage / commissionAmount | Platform commission tracking |
createdBy / updatedBy | Audit trail — user UUID who performed the operation |
Architecture
The module follows strict Hexagonal Architecture:
domain/
service-bookings-repository.domain.ts ← IServiceBookingsRepository port + SERVICE_BOOKINGS_REPOSITORY token
application/
service-bookings.service.ts ← Orchestrates use cases (depends on domain port via @Inject)
events/
on-create-service-booking.event.ts ← Emitted after creation
on-update-service-booking.event.ts ← Emitted after update
infrastructure/
service-bookings.repository.ts ← Kysely adapter (implements the port)
interfaces/
service-bookings.controller.ts ← HTTP REST adapter
dtos/
create-service-booking.dto.ts
update-service-booking.dto.ts
query/
paginate-service-bookings.query.ts
Dependency flow
interfaces → application → domain ← infrastructure
Infrastructure depends on domain (implements its port); the application layer depends only on the domain interface. The repository is wired via a Symbol-based injection token (SERVICE_BOOKINGS_REPOSITORY) defined in the domain layer.
Use Cases
| Use Case | Method | HTTP |
|---|---|---|
| Create booking | createServiceBooking | POST /service-bookings |
| List bookings (paginated) | getAllServiceBookings | GET /service-bookings |
| Get booking by ID | getServiceBookingById | GET /service-bookings/:id |
| Update booking | updateServiceBooking | PATCH /service-bookings/:id |
| Delete booking | deleteServiceBooking | DELETE /service-bookings/:id |
| Download PDF | generateServiceBookingPdf | GET /service-bookings/:id/pdf |
| Get PDF as base64 | generateServiceBookingPdfContent | GET /service-bookings/:id/pdf/content |
| HTML print view | generateServiceBookingPrintView | GET /service-bookings/:id/print |
Authorization
- Authentication: Bearer token (Firebase) — validated by global
AuthGuard - Authorization:
RolesGuard+ CASL permissions- Class-level:
@PermissionResource(PolicyResource.ServiceBooking) - Method-level:
@PermissionAction(PolicyAction.Create | Read | Update | Delete)
- Class-level:
- Swagger:
@ApiBearerAuth()marks all endpoints as requiring authentication
API Endpoints
POST /service-bookings — Create Booking
Permission: ServiceBooking:Create
Request body:
{
"businessId": "<business-uuid>",
"createdBy": "<user-uuid>",
"bookingPlatformId": "<platform-uuid>",
"serviceTypeId": "<service-type-uuid>",
"externalRef": "SB-2024-001",
"numberOfParticipants": 2,
"childrenNumber": 0,
"occurredAt": "2024-01-15T10:30:00Z",
"serviceAt": "2024-01-15T10:30:00Z",
"notes": "VIP customer",
"currencyId": "<currency-uuid>",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00,
"currency": { "id": "<uuid>", "name": "Quetzal", "symbol": "Q" },
"totalAmount": 5000,
"totalBaseAmount": 5000,
"detail": { "serviceType": "consultation", "duration": 60 },
"grossAmount": 5000,
"commissionPercentage": 5.0,
"commissionAmount": 250
}
Response: 201 Created — full service booking object with auto-generated documentNumber.
GET /service-bookings — List Bookings
Permission: ServiceBooking:Read
Query params:
| Param | Required | Description |
|---|---|---|
businessId | yes | UUID — scopes the query |
page | no | Page number (default: 1) |
size | no | Page size (default: 20, 0 = all) |
search | no | Full-text search across booking platform name, service type name, external reference, notes |
orderBy | no | Column to sort by |
order | no | asc or desc |
Sortable columns: bookingPlatformId, externalRef, createdAt, serviceAt, documentNumber
Response: 200 OK — IOffsetPagination<ServiceBooking>
GET /service-bookings/:id — Get by ID
Permission: ServiceBooking:Read
Response: 200 OK — service booking object, or 404 Not Found.
PATCH /service-bookings/:id — Update Booking
Permission: ServiceBooking:Update
Request body (all fields except updatedBy are optional):
{
"updatedBy": "<user-uuid>",
"notes": "Updated notes",
"numberOfParticipants": 3
}
Response: 200 OK — updated service booking object.
DELETE /service-bookings/:id — Delete Booking
Permission: ServiceBooking:Delete
Response: 200 OK — deleted service booking object. Returns 404 if not found.
GET /service-bookings/:id/pdf — Download PDF
Permission: ServiceBooking:Read
Optional query params: templateId, locationId, useLocationTemplate
Response: 200 OK — PDF file with Content-Disposition: attachment.
GET /service-bookings/:id/pdf/content — PDF as Base64
Permission: ServiceBooking:Read
Response:
{
"filename": "serviceBooking-SB001-2024-01-15.pdf",
"content": "<base64-string>",
"mimeType": "application/pdf",
"sizeBytes": 12345
}
GET /service-bookings/:id/print — HTML Print View
Permission: ServiceBooking:Read
Response: 200 OK — HTML string (Content-Type: text/html).
Events
| Event | Name | Payload |
|---|---|---|
| Booking created | service-booking.create | { createdServiceBooking } |
| Booking updated | service-booking.update | { originalServiceBookingRecord, updatedServiceBookingRecord } |
Design Decisions
Hard delete
DELETE performs a hard delete (deleteFrom). There is no soft-delete flag.
Auto-generated document number
The createServiceBooking use case generates a sequential document number within a transaction using DocumentType.SERVICE_BOOKING.
Dual-currency support
Each booking stores both the booking currency and an optional customer-paid currency. This handles scenarios where the customer pays in a different currency than the booking platform uses.
Symbol-based repository injection
The repository is bound via SERVICE_BOOKINGS_REPOSITORY Symbol token in the module, and injected into the service via @Inject(SERVICE_BOOKINGS_REPOSITORY). This ensures the application layer depends only on the domain port (IServiceBookingsRepository), not the concrete Kysely implementation.
Bruno API Collection
Located at: api-client/flowpos/collections/service-bookings/
| File | Method | Route |
|---|---|---|
service bookings.yml | GET | /service-bookings |
service booking.yml | POST | /service-bookings |
service booking by Id.yml | GET | /service-bookings/:id |
update-service-booking.yml | PATCH | /service-bookings/:id |
delete-service-booking.yml | DELETE | /service-bookings/:id |
service booking by Id - pdf.yml | GET | /service-bookings/:id/pdf |
service booking by Id - pdf content.yml | GET | /service-bookings/:id/pdf/content |
service booking by Id - print.yml | GET | /service-bookings/:id/print |
Environment variables used: BASE_URL, ID_TOKEN, businessId, userId, bookingPlatformId, serviceTypeId, currencyIdGtq, currencyCodeGtq, minorUnitGtq, currencyNameGtq, currencySymbolGtq, saleId, serviceBookingId.