Saltar al contenido principal

Inventory Ledgers

Overview

The Inventory Ledgers module records every stock movement as an immutable ledger entry. It functions as the single source of truth for inventory event history, supporting purchases, sales, adjustments, transfers, returns, production, damage reports, and reservations.

Every ledger entry captures:

  • What moved (product, variant, quantity, batch/serial)
  • Where it moved (business, location, stock bucket)
  • Why it moved (source type, source document, movement type)
  • How much it cost (unit cost, unit price, amounts in both transaction and base currencies)

Architecture

inventory-ledgers/
├── inventory-ledgers.module.ts # NestJS module
├── domain/
│ └── inventory-ledgers-repository.domain.ts # Port interface + sortable keys
├── application/
│ └── inventory-ledgers.service.ts # Use cases
├── infrastructure/
│ └── inventory-ledgers.repository.ts # Kysely adapter
└── interfaces/
├── inventory-ledgers.controller.ts # REST controller
├── dtos/
│ ├── create-inventory-ledger.dto.ts
│ └── update-inventory-ledger.dto.ts
└── query/
└── paginate-inventory-ledgers.query.ts

Layer Responsibilities

LayerResponsibility
DomainRepository interface (port), sortable field definitions. Framework-agnostic.
ApplicationUse cases: create (single + batch), paginate, get by ID, update, delete. Idempotency key auto-generation.
InfrastructureKysely implementation of the repository port. Handles SQL queries, pagination, search, filtering.
InterfacesREST controller (5 endpoints), DTOs with validation, query objects.

Dependency Injection

The repository is injected via the INVENTORY_LEDGERS_REPOSITORY symbol token, allowing the service to depend on the domain interface rather than the concrete Kysely implementation.

Database

Table: inventory_ledger

Key Columns

ColumnTypeDescription
iduuid (PK)Auto-generated
businessIduuid (FK)Multi-tenancy scope
locationIduuid (FK)Where the movement occurred
productIduuid (FK)Product affected
sourceTypestringDocument type (purchase, sale, adjustment, etc.)
sourceIduuidSource document reference
sourceLineIduuidSource line item reference
quantitydecimalQuantity moved (positive = in, negative = out)
unitCost / unitPricedecimalPer-unit financials
amount / baseAmountdecimalTotal in transaction / base currency
cost / baseCostdecimalTotal cost in transaction / base currency
movementTypeenumsale_out, return_in, adjustment, transfer_out, transfer_in, damage_mark, damage_restore, quarantine_mark, quarantine_release, reservation_hold, reservation_release
stockBucketenumon_hand, damaged, quarantine, reserved
batchNumber / serialNumberstringTraceability
idempotencyKeystring (unique)Duplicate prevention
saleId / saleLineIndexuuid / intReturn/exchange tracking

Indexes

  • idx_inventory_ledger_business (businessId)
  • idx_inventory_ledger_location (locationId)
  • idx_inventory_ledger_sale (saleId)
  • idx_inventory_ledger_sale_line (saleId, saleLineIndex)
  • idx_inventory_ledger_movement_type (movementType)
  • idx_inventory_ledger_stock_bucket (stockBucket)
  • Unique partial index on (businessId, serialNumber, movementType) WHERE movementType = 'return_in'

API Endpoints

MethodPathDescription
POST/inventory-ledgersCreate a ledger entry
GET/inventory-ledgersList with pagination, search, filtering
GET/inventory-ledgers/:idGet by ID
PATCH/inventory-ledgers/:idPartial update (requires updatedBy)
DELETE/inventory-ledgers/:idDelete entry

Query Parameters (GET list)

ParamTypeDescription
businessIdUUIDFilter by business
locationIdUUIDFilter by location
productIdUUIDFilter by product
searchstringFull-text search (sourceType, batchNumber, serialNumber, locationName, productName)
pagenumberPage number (default: 1)
sizenumberPage size (default: 10, 0 = no limit)
orderBystringSort field
orderstringasc or desc

Consumers

This module is used by multiple other modules via InventoryLedgersService:

ModuleUsage
data-importBatch creates ledger rows for opening inventory imports
production-runsTracks material consumption and output
retail-reservationsRecords reservation_hold / reservation_release movements
retail-returnRecords return_in movements and restocking
stock-countRecords adjustment movements from inventory counts
damage-reportsRecords damage_mark / damage_restore / quarantine movements
inventoriesPrimary consumer — coordinates stock quantities with ledger

Idempotency

Every ledger entry has a unique idempotencyKey:

  • Single creates (via API): auto-generated UUID if not provided by client
  • Batch creates (via createByRowsWithTransaction): uses ON CONFLICT (idempotencyKey) DO NOTHING to silently skip duplicates

Design Decisions

  1. Immutable-first: Ledger entries are append-only in practice. Update/delete endpoints exist for admin corrections but are not part of normal business flows.
  2. Denormalized names: locationName and productName are stored directly for historical accuracy and offline reporting (the product or location may be renamed later).
  3. Dual currency: All financial fields exist in both transaction currency (amount, cost) and base/reporting currency (baseAmount, baseCost).
  4. Movement type + stock bucket: Added to support return/exchange, damage, and reservation workflows. Nullable for backward compatibility with pre-existing entries.