Saltar al contenido principal

Retail Reservations

Overview

The Retail Reservations module allows store staff to reserve inventory for customers before a sale is completed. A reservation holds specific product quantities at a location, preventing other sales from consuming that stock during the hold period.

This is common in apparel/retail scenarios where a customer requests items be set aside for pickup or where a sales associate wants to guarantee availability.

Domain Concepts

ConceptDescription
ReservationA header document scoped to a business + location, optionally linked to a customer.
Reservation LineAn individual product + quantity within a reservation.
Reserved StockInventory moved from the on_hand bucket to the reserved bucket while a reservation is active.

Lifecycle (State Machine)

DRAFT ──── activate ───► ACTIVE
│ │
└──── cancel ─────────────┼──► CANCELED

fulfill ──► FULFILLED

[auto-expire] ──► EXPIRED
TransitionFromToInventory Effect
activateDRAFTACTIVEon_hand → reserved (per line)
cancelDRAFTCANCELEDNone
cancelACTIVECANCELEDreserved → on_hand (release)
fulfillACTIVEFULFILLEDreserved → on_hand (release, then POS sale deducts normally)
expireACTIVEEXPIREDreserved → on_hand (automatic, background job)

Architecture

The module follows Hexagonal Architecture:

domain/
retail-reservations-repository.domain.ts # Port (interface + injection token)

application/
retail-reservations.service.ts # Use cases (create, activate, cancel, fulfill, expire)
processors/
retail-reservations-expire.processor.ts # BullMQ scheduled job (every 15 min)

infrastructure/
retail-reservations.repository.ts # Kysely adapter implementing the domain port

interfaces/
retail-reservations.controller.ts # HTTP adapter (REST endpoints)
dtos/ # Request validation
query/ # Query parameter validation

Dependencies

  • InventoriesModule — stock bucket operations (decreaseInventoryToReserveStock, releaseReservedStockToOnHand)
  • InventoryLedgersModule — audit trail for all inventory movements
  • BullMQ — background job queue for auto-expiry

API Endpoints

MethodPathDescription
POST/retail-reservationsCreate a DRAFT reservation
GET/retail-reservationsList reservations (paginated, filterable)
GET/retail-reservations/:idGet reservation with lines
PATCH/retail-reservations/:idUpdate a DRAFT reservation
POST/retail-reservations/:id/activateActivate (hold stock)
POST/retail-reservations/:id/cancelCancel (release stock if active)
POST/retail-reservations/:id/fulfillFulfill (release stock for POS sale)

Query Parameters (GET list)

ParamTypeRequiredDescription
businessIdUUIDYesScope to business
locationIdUUIDNoFilter by store
statusenumNodraft, active, fulfilled, canceled, expired
customerIdUUIDNoFilter by customer
expiresAtFromISO dateNoExpiry range start
expiresAtToISO dateNoExpiry range end
pagenumberNoPage number (default 1)
sizenumberNoPage size (default 20)

Database Tables

retail_reservation (header)

Key columns: id, businessId, locationId, customerId, status, expiresAt, reservationNumber, fulfilledSaleId, contactName, contactPhone, notes, createdBy, updatedBy, timestamps.

retail_reservation_line (items)

Key columns: id, reservationId, businessId, locationId, productId, productName, qty, notes, createdAt.

Inventory Ledger Integration

All stock movements create inventory_ledger entries:

  • Movement types: reservation_hold (activate), reservation_release (cancel/fulfill/expire)
  • Stock buckets: reserved (hold), on_hand (release)
  • Reference/source: retail_reservation with sourceLineId linking to the specific line

Auto-Expiry

A BullMQ repeatable job runs every 15 minutes:

  1. Queries all ACTIVE reservations where expiresAt < now()
  2. For each, releases reserved stock back to on_hand
  3. Updates status to EXPIRED
  4. Errors on individual reservations are logged but do not block other expirations

Design Decisions

  1. Fulfill releases to on_hand — Rather than deducting directly from reserved, fulfill releases stock back to on_hand so the standard POS sale flow can deduct normally. This avoids duplicating sale logic.

  2. Draft → Active two-step — Reservations are created as DRAFT first, allowing edits before stock is committed. Activation is an explicit action that validates stock availability.

  3. Contact info alongside customercontactName and contactPhone support walk-in customers who may not have a system account, while customerId links to registered customers.

  4. Idempotency keys on ledger entries — Each ledger row gets a unique idempotencyKey to prevent double-processing in retry scenarios.