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
| Concept | Description |
|---|---|
| Reservation | A header document scoped to a business + location, optionally linked to a customer. |
| Reservation Line | An individual product + quantity within a reservation. |
| Reserved Stock | Inventory 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
| Transition | From | To | Inventory Effect |
|---|---|---|---|
| activate | DRAFT | ACTIVE | on_hand → reserved (per line) |
| cancel | DRAFT | CANCELED | None |
| cancel | ACTIVE | CANCELED | reserved → on_hand (release) |
| fulfill | ACTIVE | FULFILLED | reserved → on_hand (release, then POS sale deducts normally) |
| expire | ACTIVE | EXPIRED | reserved → 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
| Method | Path | Description |
|---|---|---|
POST | /retail-reservations | Create a DRAFT reservation |
GET | /retail-reservations | List reservations (paginated, filterable) |
GET | /retail-reservations/:id | Get reservation with lines |
PATCH | /retail-reservations/:id | Update a DRAFT reservation |
POST | /retail-reservations/:id/activate | Activate (hold stock) |
POST | /retail-reservations/:id/cancel | Cancel (release stock if active) |
POST | /retail-reservations/:id/fulfill | Fulfill (release stock for POS sale) |
Query Parameters (GET list)
| Param | Type | Required | Description |
|---|---|---|---|
businessId | UUID | Yes | Scope to business |
locationId | UUID | No | Filter by store |
status | enum | No | draft, active, fulfilled, canceled, expired |
customerId | UUID | No | Filter by customer |
expiresAtFrom | ISO date | No | Expiry range start |
expiresAtTo | ISO date | No | Expiry range end |
page | number | No | Page number (default 1) |
size | number | No | Page 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_reservationwithsourceLineIdlinking to the specific line
Auto-Expiry
A BullMQ repeatable job runs every 15 minutes:
- Queries all ACTIVE reservations where
expiresAt < now() - For each, releases reserved stock back to on_hand
- Updates status to EXPIRED
- Errors on individual reservations are logged but do not block other expirations
Design Decisions
-
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.
-
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.
-
Contact info alongside customer —
contactNameandcontactPhonesupport walk-in customers who may not have a system account, whilecustomerIdlinks to registered customers. -
Idempotency keys on ledger entries — Each ledger row gets a unique
idempotencyKeyto prevent double-processing in retry scenarios.