Saltar al contenido principal

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

ConceptDescription
ServiceBookingA record of a booked service with platform, type, participants, financials, and dates
businessIdMulti-tenancy boundary. Every query filters by this field
bookingPlatformIdReferences the bookingPlatform table (e.g., Airbnb, Booking.com)
serviceTypeIdReferences the serviceType table
externalRefExternal reference identifier from the booking platform
documentNumberAuto-generated sequential document number
detailFlexible JSON structure for service item breakdown
currency / customerPaidCurrencyDual-currency support: booking currency and customer-paid currency
commissionPercentage / commissionAmountPlatform commission tracking
createdBy / updatedByAudit 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 CaseMethodHTTP
Create bookingcreateServiceBookingPOST /service-bookings
List bookings (paginated)getAllServiceBookingsGET /service-bookings
Get booking by IDgetServiceBookingByIdGET /service-bookings/:id
Update bookingupdateServiceBookingPATCH /service-bookings/:id
Delete bookingdeleteServiceBookingDELETE /service-bookings/:id
Download PDFgenerateServiceBookingPdfGET /service-bookings/:id/pdf
Get PDF as base64generateServiceBookingPdfContentGET /service-bookings/:id/pdf/content
HTML print viewgenerateServiceBookingPrintViewGET /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)
  • 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:

ParamRequiredDescription
businessIdyesUUID — scopes the query
pagenoPage number (default: 1)
sizenoPage size (default: 20, 0 = all)
searchnoFull-text search across booking platform name, service type name, external reference, notes
orderBynoColumn to sort by
ordernoasc or desc

Sortable columns: bookingPlatformId, externalRef, createdAt, serviceAt, documentNumber

Response: 200 OKIOffsetPagination<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

EventNamePayload
Booking createdservice-booking.create{ createdServiceBooking }
Booking updatedservice-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/

FileMethodRoute
service bookings.ymlGET/service-bookings
service booking.ymlPOST/service-bookings
service booking by Id.ymlGET/service-bookings/:id
update-service-booking.ymlPATCH/service-bookings/:id
delete-service-booking.ymlDELETE/service-bookings/:id
service booking by Id - pdf.ymlGET/service-bookings/:id/pdf
service booking by Id - pdf content.ymlGET/service-bookings/:id/pdf/content
service booking by Id - print.ymlGET/service-bookings/:id/print

Environment variables used: BASE_URL, ID_TOKEN, businessId, userId, bookingPlatformId, serviceTypeId, currencyIdGtq, currencyCodeGtq, minorUnitGtq, currencyNameGtq, currencySymbolGtq, saleId, serviceBookingId.