Saltar al contenido principal

Inventory Counts Module

Overview

The inventory-counts module manages physical inventory counting documents with cost discrepancy analysis. Unlike simple physical counts, this module calculates expected vs actual quantities and costs by cross-referencing current inventory data at the time of count creation.

Domain Concepts

  • Inventory Count: A document recording a physical count event at a specific location, with variance analysis against system inventory.
  • Count Type: FULL (all items at a location) or CYCLE (a targeted subset of items).
  • Detail: A JSON payload containing an items array — each item has a productId, quantity (actual counted), locationId, and product metadata.
  • Cost Detail: A frozen snapshot (costDetail JSON) created at document time capturing:
    • documentCost — grouped items with calculated discrepancy and cost fields
    • inventoryCosts — raw inventory rows at the time of count
  • Discrepancy: The difference between the actual counted quantity and the system's expected quantity. discrepancyCost = discrepancy × unitCost.

Architecture

Follows hexagonal architecture (ports & adapters):

inventory-counts/
├── domain/
│ └── inventory-counts-repository.domain.ts # Port interface + injection token
├── application/
│ ├── inventory-counts.service.ts # Use cases (business logic)
│ └── events/
│ └── on-create-inventory-count.event.ts # Domain event
├── infrastructure/
│ └── inventory-counts.repository.ts # Kysely DB adapter
└── interfaces/
├── inventory-counts.controller.ts # HTTP adapter (REST)
├── dtos/
│ ├── create-inventory-count.dto.ts
│ └── update-inventory-count.dto.ts
└── query/
└── paginate-inventory-counts.query.ts

Key dependency flow: Controller → Service → Repository (via domain port interface, injected by token).

The module imports InventoriesModule to access InventoriesRepository.findQuantityAndCostsByProductIds() for cost lookups during count creation.

Main Use Cases

Create Inventory Count

  1. Extract items from the detail.items JSON payload
  2. Group items by locationId + productId (handles duplicate entries)
  3. Fetch current inventory quantities and costs from the inventory table
  4. Calculate discrepancies: actual quantity - expected quantity
  5. Calculate discrepancy costs: discrepancy × unit cost
  6. Snapshot all costs into costDetail JSON
  7. Persist the inventory count record
  8. Emit inventoryCount.create event for downstream consumers

List Inventory Counts

Paginated listing with:

  • Search by locationName (case-insensitive)
  • Sort by: locationName, countDate, createdAt, status, countType
  • Default sort: id DESC

Get / Update / Delete

Standard CRUD operations by ID.

API Endpoints

MethodPathDescription
POST/inventory-countsCreate a new inventory count
GET/inventory-countsList inventory counts (paginated)
GET/inventory-counts/:idGet a single inventory count
PATCH/inventory-counts/:idPartially update an inventory count
DELETE/inventory-counts/:idDelete an inventory count

Query Parameters (GET list)

ParamTypeDefaultDescription
searchstringCase-insensitive search on locationName
pagenumber1Page number
sizenumber10Page size (0 = no limit)
orderBystringSort field: locationName, countDate, createdAt, status, countType
orderstringascSort direction: asc or desc

Create Request Example

POST /inventory-counts
{
"businessId": "uuid",
"status": "ACTIVE",
"createdBy": "uuid",
"countDate": "2026-03-25T00:00:00.000Z",
"locationId": "uuid",
"locationName": "Main Warehouse",
"totalAmount": 0,
"exchangeRate": 1.00,
"totalBaseAmount": 0,
"currencyId": "uuid",
"detail": {
"items": [
{
"id": "uuid",
"productId": "uuid",
"productName": "COCA COLA",
"quantity": 9,
"locationId": "uuid",
"locationName": "Main Warehouse"
}
]
},
"countType": "FULL",
"notes": "Monthly full count"
}

Response (Created)

The response includes all fields from the request plus:

  • id — generated UUID
  • createdAt — server timestamp
  • costDetail — computed cost snapshot with discrepancies

Database Schema

Table: inventory_count

ColumnTypeDescription
idUUID (PK)Auto-generated
business_idUUID (FK)Multi-tenancy scope
location_idUUID (FK)Location where count occurred
location_nameVARCHARDenormalized location name
count_dateTIMESTAMPTZDate of physical count
count_typeVARCHARFULL or CYCLE
statusVARCHARe.g., ACTIVE, INACTIVE
detailJSONBItems array with quantities
cost_detailJSONBFrozen cost snapshot with discrepancies
total_amountNUMERICTotal counted amount
total_base_amountNUMERICAmount in base currency
exchange_rateNUMERIC(20,8)FX rate to base currency
currency_idUUID (FK)Currency reference
variant_idUUID (FK)Optional product variant reference
notesTEXTFree-text notes
document_numberVARCHARHuman-readable document number
created_byUUID (FK)Creator reference
created_atTIMESTAMPTZAuto-generated
updated_byUUID (FK)Last updater
updated_atTIMESTAMPTZLast update timestamp

Design Decisions

  1. Cost snapshot at creation: Costs are frozen into costDetail at creation time to prevent retroactive changes from affecting historical count accuracy.
  2. Discrepancy calculation: The system calculates expectedQuantity from current inventory and discrepancy = actual - expected, enabling variance reports without re-querying.
  3. Event-driven side effects: OnCreateInventoryCountEvent is emitted after creation, allowing inventory adjustment handlers to react without coupling.
  4. Token-based repository injection: Uses INVENTORY_COUNTS_REPOSITORY symbol for proper dependency inversion following hexagonal architecture.
  • inventories — Provides current stock quantities/costs for discrepancy calculation
  • inventory-details — Granular inventory detail records
  • inventory-ledgers — Audit trail of inventory movements
  • physical-counts — Simpler physical count documents (separate table)