Saltar al contenido principal

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:

BucketDescription
quantity / costOn-hand available stock
reservedStock / reservedStockCostPre-allocated for pending transfers or replacements
inTransitIncoming / inTransitIncomingCostStock en route from supplier or other location
inTransitOutgoing / inTransitOutgoingCostStock dispatched but not yet received at destination
pendingInspection / pendingInspectionCostReturned items awaiting quality check
damaged / damagedCostItems deemed damaged after inspection
underRepair / underRepairCostItems sent for repair
quarantined / quarantinedCostItems held in quarantine
toBeRefurbished / toBeRefurbishedCostItems queued for refurbishment
refurbishing / refurbishingCostItems currently being refurbished
quantityInventoryDetailTracked 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
  • ProductCostHistory tracks WAC changes for audit trail

Variant Support

  • Products with hasVariants=true require variantId for inventory rows
  • Each (location, product, variant) tuple has a distinct inventory record
  • Bulk creation endpoint for multi-variant setup

API Endpoints

MethodPathDescription
POST/inventoriesCreate inventory record
POST/inventories/bulkBulk create for product variants
GET/inventoriesList with pagination, search, filters
GET/inventories/:idGet by ID
GET/inventories/low-stockDetect low-stock items
PATCH/inventories/:idUpdate (price, reorder settings, etc.)
DELETE/inventories/:idDelete record

Query Parameters (GET /inventories)

  • businessId (required) - UUID
  • locationId (optional) - filter by location
  • productId (optional) - filter by product
  • variantId (optional) - filter by variant
  • search (optional) - search by product name, description, SKU, barcode
  • page, 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

  1. InventoryTransfer (APPROVED) -> decreaseInventoryToReserveStock (origin)
  2. TransferDispatchNote (APPROVED) -> decreaseReservedStockToInTransitOutgoing (origin) + increaseInTransitIncoming (destination)
  3. 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

  1. Atomic SQL cost calculations: All cost updates use SQL expressions (ROUND, GREATEST, COALESCE) to prevent race conditions. No read-then-write pattern.

  2. Auto-creation of inventory rows: ensureInventoryRecordsExist creates 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.

  3. Idempotent ledger rows: Each ledger entry has an MD5-based idempotencyKey derived from (sourceType, businessId, documentId, lineId, productId, location, batch, serial, date, qty, cost). Prevents duplicate entries on event replay.

  4. WAC utility extraction: updateProductCostsFromReceipt and updateProductCostsFromSupplierReturn are shared utilities that handle both variant and simple products. Used by GRN, Purchase, and ReplacementIssueNote flows.