Restaurant Module — Architecture Reference
Quick links: Restaurant.md (domain concepts) · kds-tracking-model.md (KDS workflow) · Bruno collections
Purpose
The restaurant module owns the full table-service lifecycle:
- Dining room layout (areas and tables)
- Menus, modifiers, and price rules
- Order creation, item management, and kitchen routing
- Split bills, payments, and FEL certification
- KDS (Kitchen Display System) devices and ticket lifecycle
- Thermal printer management (network ESC/POS)
- Product recipes (BOM) and inventory deduction
- Cash shifts (open/drop/close/reconcile)
- Reservations and waitlist
- Reporting (product mix, ingredient depletion, shift summaries)
- External platform mappings (UberEats, DoorDash, etc.)
Module Layout
apps/backend/src/restaurant/
├── restaurant.module.ts Root module — imports 10 feature sub-modules
├── domain/ Pure TypeScript interfaces (no framework deps)
├── application/ @Injectable() services — business logic
├── infrastructure/ Repositories, gateways, adapters, processors
├── interfaces/ Controllers + DTOs
└── modules/ Feature sub-modules (dining, order, kitchen …)
Sub-module dependency graph
RestaurantModule
├── DiningModule (no external deps)
├── MenuModule (no external deps)
├── PriceRuleModule (no external deps)
├── RecipeModule (no external deps)
├── ReservationModule (no external deps)
├── ShiftModule (no external deps)
├── ReportsModule (no external deps)
├── ExternalPlatformModule (no external deps)
├── KitchenModule ← ForwardRef(OrderModule) + BullMQ (2 queues)
└── OrderModule ← Bundles, Discounts, Pricing, FEL, Inventory,
CashRegister, PDF, Employees + ForwardRef(KitchenModule)
OrderModule has the broadest dependency graph because order processing touches pricing, tax, discounts, bundling, inventory, and FEL (electronic invoicing). The ForwardRef cycle between OrderModule and KitchenModule exists because sending an order to kitchen creates print jobs (kitchen concern) while bumping a ticket updates order item statuses (order concern).
Layer Boundaries
Domain (domain/)
Pure TypeScript — zero NestJS imports. Contains:
- Repository interfaces (ports):
IOrderRepository,IKitchenStationRepository, etc. - Port interfaces for external services:
IPrinterService,IKdsNotificationService - Shared types used across layers:
KitchenTicketData,RecipeSnapshotJsonb
Injection tokens are exported as const symbols (e.g. ORDER_REPOSITORY) to allow NestJS DI without importing framework types into the domain.
Application (application/)
All services are @Injectable(). They depend only on domain interfaces injected via @Inject(SYMBOL), never on concrete infrastructure classes.
Key services:
| Service | Responsibility |
|---|---|
OrdersService | Core order lifecycle: create, update, add/void/comp items, send to kitchen, discounts, bundles, split config |
OrderBillService | Split bills, FEL certification, QR code generation |
OrderBillPaymentService | Record payments, auto-mark bills paid |
KitchenStationService | Station CRUD, product-station routing, printer health, pairing codes |
KitchenTicketService | Bump/recall tickets, sync with order item status |
PrintJobService | Create and query async print jobs |
RestaurantShiftService | Open/drop/close shifts, reconcile variance |
ProductRecipeService | BOM create/replace/delete |
MenuService | Menus, menu items, location assignments |
DiningAreaService / DiningTableService | Room layout CRUD |
Stateless pure services — no DB calls, fully deterministic, easy to unit-test:
| Service | Purpose |
|---|---|
ModifierValidationService | Validates modifier selections against group constraints |
PriceRuleEvaluationService | Evaluates price rules against a product (lowest-price-wins) |
BomDeductionService | Calculates ingredient deductions from a recipe snapshot |
ShiftReconciliationService | Calculates shift variance (expected cash vs. counted) |
TicketRoutingService | Resolves which kitchen stations an order item should route to |
Infrastructure (infrastructure/)
- Repositories — Kysely implementations of domain interfaces. Named
*.repository.ts. - Gateways — Socket.IO WebSocket servers:
RestaurantGateway— namespace/restaurant— broadcasts order/table/print-job updatesKdsGateway— namespace/kds— pushes kitchen tickets to KDS displays
- Adapters:
NetworkThermalPrinterAdapter— HTTP POST ESC/POS to network printersStubPrinterAdapter— no-op, used in development/testingPrinterAdapterFactory— selects adapter based on station configWebsocketKdsNotificationAdapter— implementsIKdsNotificationServicevia Socket.IO
- Processors (BullMQ):
DeliverKitchenTicketProcessor— async delivery with retry/backoffPrinterHealthProcessor— periodic health check, broadcasts online/offline events
- Guards:
KdsDeviceGuard— validates device token for KDS WebSocket and REST bump/recall endpoints
Interfaces (interfaces/)
15 thin controllers — each maps HTTP verbs to application service calls. No business logic.
| Controller | Base route | Tag |
|---|---|---|
OrdersController | /orders | Restaurant Orders |
OrderBillsController | /order-bills, /orders/:id/dispatch | Restaurant Bills |
KitchenController | /kitchen-stations, /tickets, /devices, /stations, /print-jobs | Restaurant Kitchen |
DiningController | /dining-areas, /dining-tables | Restaurant Dining |
MenusController | /menus, /menu-items | Restaurant Menus |
ProductModifiersController | /product-modifier-groups, /product-modifiers | Restaurant Modifiers |
PriceRulesController | /price-rules | Restaurant Price Rules |
RecipeController | /restaurant/recipes | Restaurant Recipes |
ShiftController | /restaurant/shifts | Restaurant Shifts |
ReportsController | /restaurant/reports | Restaurant Reports |
ReservationsController | /restaurant/reservations | Restaurant Reservations |
WaitlistController | /restaurant/waitlist | Restaurant Waitlist |
OrderGuestsController | /orders/:id/guests | (nested in Orders) |
OrderPartyController | /orders/:id/parties | (nested in Orders) |
ExternalPlatformMappingsController | /external-platform-mappings | Restaurant External Platforms |
Key Flows
1. Order creation → kitchen
POST /orders
→ OrdersService.createOrder()
→ generateDocumentNumber() [Kysely transaction]
→ validate waiter (EmployeesService)
→ validate table (DiningTableRepository)
→ insert order row
POST /orders/:id/send-to-kitchen
→ OrdersService.sendOrderToKitchen()
→ Kysely transaction:
→ mark pending items as "sent_to_kitchen"
→ PrintJobService.createPrintJobs()
→ emit OnOrderSentToKitchenEvent
OnOrderSentToKitchenHandler
→ TicketRoutingService.routeTickets()
→ DeliverKitchenTicketProcessor (BullMQ)
→ KdsNotificationAdapter.pushTicket() → KdsGateway → WS /kds
→ OR NetworkThermalPrinterAdapter.print() → ESC/POS HTTP
2. Bill splitting and payment
POST /order-bills { orderId, mappings?, splitBy? }
→ OrderBillService.createBill()
→ calculate item→bill split
→ insert order_bill + order_bill_mapping rows
→ optionally trigger FEL certification (FelService)
POST /order-bills/:id/payments { amount, paymentMethodId, cashRegisterSessionId }
→ OrderBillPaymentService.recordPayment()
→ insert order_bill_payment
→ if sum(payments) >= bill.total → mark bill PAID
→ if all bills PAID → mark order PAID → emit OnOrderSettledEvent
OnOrderSettledHandler
→ BomDeductionService.calculateDeductions()
→ InventoryService.deductIngredients()
3. KDS bump → order item completion
POST /tickets/:id/bump (KdsDeviceGuard — device token auth)
→ KitchenTicketService.bumpTicket()
→ mark ticket "bumped"
→ OrdersService.syncOrderItemStatusFromKitchenTickets()
→ if all station tickets bumped → mark item "served"
→ if all items served → mark order "served"
→ emit realtime update via RestaurantGateway
Real-time Events
All WebSocket events are emitted via EventEmitter2 and handled by RestaurantRealtimeHandler, which broadcasts them over Socket.IO.
| Event class | Trigger | WS room |
|---|---|---|
OnRestaurantOrderCreatedEvent | createOrder | business:{businessId} |
OnRestaurantOrderUpdatedEvent | updateOrder, status changes | business:{businessId} |
OnRestaurantOrderItemUpdatedEvent | item add/void/comp/fire | business:{businessId} |
OnOrderSentToKitchenEvent | sendOrderToKitchen | triggers KDS delivery |
OnRestaurantOrderReadyEvent | all items served | business:{businessId} |
Domain Layer Highlights
Order status state machine (domain/order-status.domain.ts)
Valid status transitions for orders and order items are declared as readonly constants in the domain layer:
ORDER_STATUS_TRANSITIONS['sent_to_kitchen'] // → ['partially_served', 'served', 'cancelled']
ORDER_ITEM_STATUS_TRANSITIONS['preparing'] // → ['ready', 'served', 'cancelled', 'pending']
Helper functions isValidOrderStatusTransition() and isValidOrderItemStatusTransition() are exported for any layer that needs guard checks without importing the whole map.
Known Design Decisions
orders.service.ts is intentionally large
OrdersService (~2,900 LOC, 30+ methods) is the core order domain orchestrator. It has not been split into sub-services because all operations share the same Kysely transaction context and need access to the same repositories. Splitting would require passing transaction handles across service boundaries, increasing coupling.
Deferred refactor: Extract OrderDiscountService, OrderBundleService, and OrderKitchenService as focused service classes once Kysely transaction propagation patterns are standardised.
Two WebSocket namespaces
/restaurant— general order/table/print-job updates for POS terminals and waiter tablets/kds— kitchen-only events (ticket created, bumped, recalled) for KDS devices
Separation avoids kitchen displays receiving irrelevant POS events and allows different auth strategies (Firebase vs KDS device token).
DELETE endpoints with body
DELETE /orders/:id/items/:itemId/discount and similar endpoints accept a body (discountIndex). This is non-standard HTTP but required because the resource is identified by array index, not by its own ID. Clients should use @ApiBody hint for these.
FEL certification in OrderBillService
FEL (Latin American electronic invoicing) certification is triggered from OrderBillService.createBill() when the business has FEL enabled. The FEL certificate becomes part of the bill record and is required before printing a legal receipt.
Testing
Unit tests live in application/__tests__/. Most tests mock domain repositories via Jest's jest.fn() injection pattern.
Integration tests (where they exist) use a real PostgreSQL instance via Docker Compose.
Run:
pnpm --filter backend run test -- --testPathPattern=restaurant
Follow-ups / Known Gaps
| # | Item | Priority |
|---|---|---|
| 1 | Split orders.service.ts into focused sub-services (OrderItemService, OrderDiscountService, OrderBundleService) | Medium |
| 2 | Add @ApiResponse schemas to remaining controllers (shift, recipe, menus, reports, reservations, waitlist, modifiers, price-rules) | Low |
| 3 | generateBridgePairingCode POST uses @Query params — consider moving to request body for consistency | Low |
| 4 | Add integration test suite for full order lifecycle (create → kitchen → bump → bill → pay) | Medium |
| 5 | Document WebSocket event payloads in Swagger or a separate WS spec | Low |
| 6 | Extract inline Redis provider in kitchen.module.ts into a shared RedisModule to avoid duplicate connections | Low |
| 7 | RestaurantRealtimeHandler handles document-print events — consider decoupling by letting each module emit via a shared gateway interface | Low |