Saltar al contenido principal

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:

ServiceResponsibility
OrdersServiceCore order lifecycle: create, update, add/void/comp items, send to kitchen, discounts, bundles, split config
OrderBillServiceSplit bills, FEL certification, QR code generation
OrderBillPaymentServiceRecord payments, auto-mark bills paid
KitchenStationServiceStation CRUD, product-station routing, printer health, pairing codes
KitchenTicketServiceBump/recall tickets, sync with order item status
PrintJobServiceCreate and query async print jobs
RestaurantShiftServiceOpen/drop/close shifts, reconcile variance
ProductRecipeServiceBOM create/replace/delete
MenuServiceMenus, menu items, location assignments
DiningAreaService / DiningTableServiceRoom layout CRUD

Stateless pure services — no DB calls, fully deterministic, easy to unit-test:

ServicePurpose
ModifierValidationServiceValidates modifier selections against group constraints
PriceRuleEvaluationServiceEvaluates price rules against a product (lowest-price-wins)
BomDeductionServiceCalculates ingredient deductions from a recipe snapshot
ShiftReconciliationServiceCalculates shift variance (expected cash vs. counted)
TicketRoutingServiceResolves 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 updates
    • KdsGateway — namespace /kds — pushes kitchen tickets to KDS displays
  • Adapters:
    • NetworkThermalPrinterAdapter — HTTP POST ESC/POS to network printers
    • StubPrinterAdapter — no-op, used in development/testing
    • PrinterAdapterFactory — selects adapter based on station config
    • WebsocketKdsNotificationAdapter — implements IKdsNotificationService via Socket.IO
  • Processors (BullMQ):
    • DeliverKitchenTicketProcessor — async delivery with retry/backoff
    • PrinterHealthProcessor — 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.

ControllerBase routeTag
OrdersController/ordersRestaurant Orders
OrderBillsController/order-bills, /orders/:id/dispatchRestaurant Bills
KitchenController/kitchen-stations, /tickets, /devices, /stations, /print-jobsRestaurant Kitchen
DiningController/dining-areas, /dining-tablesRestaurant Dining
MenusController/menus, /menu-itemsRestaurant Menus
ProductModifiersController/product-modifier-groups, /product-modifiersRestaurant Modifiers
PriceRulesController/price-rulesRestaurant Price Rules
RecipeController/restaurant/recipesRestaurant Recipes
ShiftController/restaurant/shiftsRestaurant Shifts
ReportsController/restaurant/reportsRestaurant Reports
ReservationsController/restaurant/reservationsRestaurant Reservations
WaitlistController/restaurant/waitlistRestaurant Waitlist
OrderGuestsController/orders/:id/guests(nested in Orders)
OrderPartyController/orders/:id/parties(nested in Orders)
ExternalPlatformMappingsController/external-platform-mappingsRestaurant 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 classTriggerWS room
OnRestaurantOrderCreatedEventcreateOrderbusiness:{businessId}
OnRestaurantOrderUpdatedEventupdateOrder, status changesbusiness:{businessId}
OnRestaurantOrderItemUpdatedEventitem add/void/comp/firebusiness:{businessId}
OnOrderSentToKitchenEventsendOrderToKitchentriggers KDS delivery
OnRestaurantOrderReadyEventall items servedbusiness:{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

#ItemPriority
1Split orders.service.ts into focused sub-services (OrderItemService, OrderDiscountService, OrderBundleService)Medium
2Add @ApiResponse schemas to remaining controllers (shift, recipe, menus, reports, reservations, waitlist, modifiers, price-rules)Low
3generateBridgePairingCode POST uses @Query params — consider moving to request body for consistencyLow
4Add integration test suite for full order lifecycle (create → kitchen → bump → bill → pay)Medium
5Document WebSocket event payloads in Swagger or a separate WS specLow
6Extract inline Redis provider in kitchen.module.ts into a shared RedisModule to avoid duplicate connectionsLow
7RestaurantRealtimeHandler handles document-print events — consider decoupling by letting each module emit via a shared gateway interfaceLow