Inventories Module
Overview
The Inventories module is the core stock management engine for FlowPOS. It tracks on-hand quantities, costs, and multiple stock buckets per location/product/variant tuple. It reacts to events from 10+ other modules to atomically update stock levels and create audit ledger entries.
Architecture
inventories/
├── inventories.module.ts # NestJS module definition
├── application/
│ ├── inventories.service.ts # Business logic / use cases
│ ├── utils/
│ │ ├── ensure-inventory-records.ts # Auto-creates missing inventory rows
│ │ ├── ledger-from-detail.ts # Builds ledger rows from document detail
│ │ └── update-product-costs.ts # WAC cost calculation (GRN, purchase, return)
│ ├── events/ # 15 domain events emitted by this module
│ └── handlers/ # 6 event handlers grouped by business flow
│ ├── inventory-inbound.handler.ts # GRN + Purchase events
│ ├── inventory-outbound.handler.ts # Sale + Material Consumption events
│ ├── inventory-adjustment.handler.ts # Inventory Adjustment events
│ ├── inventory-transfer.handler.ts # Transfer + Dispatch + Receipt events
│ ├── inventory-return.handler.ts # Customer Return + Repair/Replacement events
│ └── inventory-detail-and-fel.handler.ts # Inventory Detail + FEL events
├── domain/
│ └── inventories-repository.domain.ts # Repository interface (port)
├── infrastructure/
│ └── inventories.repository.ts # Kysely implementation (adapter)
└── interfaces/
├── inventories.controller.ts # REST endpoints
├── dtos/
│ ├── create-inventory.dto.ts
│ ├── update-inventory.dto.ts
│ └── create-inventories-bulk.dto.ts
└── query/
└── paginate-inventories.query.ts
Follows hexagonal architecture: domain port -> application service -> infrastructure adapter -> interface controller.
Domain Concepts
Stock Buckets
Each inventory record (location x product x variant) tracks multiple stock buckets:
| Bucket | Description |
|---|---|
quantity / cost | On-hand available stock |
reservedStock / reservedStockCost | Pre-allocated for pending transfers or replacements |
inTransitIncoming / inTransitIncomingCost | Stock en route from supplier or other location |
inTransitOutgoing / inTransitOutgoingCost | Stock dispatched but not yet received at destination |
pendingInspection / pendingInspectionCost | Returned items awaiting quality check |
damaged / damagedCost | Items deemed damaged after inspection |
underRepair / underRepairCost | Items sent for repair |
quarantined / quarantinedCost | Items held in quarantine |
toBeRefurbished / toBeRefurbishedCost | Items queued for refurbishment |
refurbishing / refurbishingCost | Items currently being refurbished |
quantityInventoryDetail | Tracked count from batch/serial inventory details |
Cost Accounting
- WAC (Weighted Average Cost): Used for all cost calculations
- All cost updates are atomic in SQL (no read-then-write races)
- Cost follows quantity on bucket transfers
ProductCostHistorytracks WAC changes for audit trail
Variant Support
- Products with
hasVariants=truerequirevariantIdfor inventory rows - Each (location, product, variant) tuple has a distinct inventory record
- Bulk creation endpoint for multi-variant setup
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /inventories | Create inventory record |
POST | /inventories/bulk | Bulk create for product variants |
GET | /inventories | List with pagination, search, filters |
GET | /inventories/:id | Get by ID |
GET | /inventories/low-stock | Detect low-stock items |
PATCH | /inventories/:id | Update (price, reorder settings, etc.) |
DELETE | /inventories/:id | Delete record |
Query Parameters (GET /inventories)
businessId(required) - UUIDlocationId(optional) - filter by locationproductId(optional) - filter by productvariantId(optional) - filter by variantsearch(optional) - search by product name, description, SKU, barcodepage,size,orderBy,order- pagination and sorting
Low-Stock Detection (GET /inventories/low-stock)
Returns products where quantity <= reorderThreshold. Uses inventory-level threshold if set (> 0), otherwise falls back to product-level threshold. Items with threshold = 0 are excluded.
Event-Driven Stock Mutations
The module listens to events from other modules and updates stock accordingly:
Inbound (increase stock)
- GoodsReceivedNote (SUBMITTED/REVIEWED) ->
increaseInventory+ WAC update - Purchase (SUBMITTED/REVIEWED) ->
increaseInventory+ WAC update - CreditNote (certified) ->
increaseInventory(FEL reversal) - FelCancellation (certified) ->
increaseInventory(sale cancellation) - CustomerReturn (RETURN type) ->
increaseInventory
Outbound (decrease stock)
- Sale (SUBMITTED/REVIEWED) ->
decreaseInventory - MaterialConsumption (SUBMITTED/REVIEWED) ->
decreaseInventory
Transfer flow
- InventoryTransfer (APPROVED) ->
decreaseInventoryToReserveStock(origin) - TransferDispatchNote (APPROVED) ->
decreaseReservedStockToInTransitOutgoing(origin) +increaseInTransitIncoming(destination) - TransferGoodsReceipt (APPROVED) ->
decreaseInTransitOutgoing(origin) +decreaseInTransitIncoming(destination, moves to on-hand)
Adjustment
- InventoryAdjustment (READY/POSTED, INCREASE) ->
increaseInventory - InventoryAdjustment (READY/POSTED, DECREASE) ->
decreaseStockByInventoryAdjustment
Returns / Quality
- CustomerReturn (REPLACEMENT/REPAIR) ->
increasePendingInspection - CustomerReturn update (DAMAGED condition) ->
decreasePendingInspection+increaseDamaged - CustomerReturn update (REPAIR condition) ->
decreasePendingInspection+increaseUnderRepair - RepairIssueNote (RETURNED) ->
decreaseUnderRepair - ReplacementIssueNote (RESERVE_STOCK) ->
decreaseInventoryToReserveStock - ReplacementIssueNote (COMPLETED) ->
decreaseReservedStock+ reverse WAC
Design Decisions
-
Atomic SQL cost calculations: All cost updates use SQL expressions (
ROUND,GREATEST,COALESCE) to prevent race conditions. No read-then-write pattern. -
Auto-creation of inventory rows:
ensureInventoryRecordsExistcreates missing rows within the transaction before updating quantities. This prevents FK violations when a document references a product/location combo that doesn't have an inventory row yet. -
Idempotent ledger rows: Each ledger entry has an MD5-based
idempotencyKeyderived from (sourceType, businessId, documentId, lineId, productId, location, batch, serial, date, qty, cost). Prevents duplicate entries on event replay. -
WAC utility extraction:
updateProductCostsFromReceiptandupdateProductCostsFromSupplierReturnare shared utilities that handle both variant and simple products. Used by GRN, Purchase, and ReplacementIssueNote flows.