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) orCYCLE(a targeted subset of items). - Detail: A JSON payload containing an
itemsarray — each item has aproductId,quantity(actual counted),locationId, and product metadata. - Cost Detail: A frozen snapshot (
costDetailJSON) created at document time capturing:documentCost— grouped items with calculated discrepancy and cost fieldsinventoryCosts— 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
- Extract items from the
detail.itemsJSON payload - Group items by
locationId + productId(handles duplicate entries) - Fetch current inventory quantities and costs from the
inventorytable - Calculate discrepancies:
actual quantity - expected quantity - Calculate discrepancy costs:
discrepancy × unit cost - Snapshot all costs into
costDetailJSON - Persist the inventory count record
- Emit
inventoryCount.createevent 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
| Method | Path | Description |
|---|---|---|
POST | /inventory-counts | Create a new inventory count |
GET | /inventory-counts | List inventory counts (paginated) |
GET | /inventory-counts/:id | Get a single inventory count |
PATCH | /inventory-counts/:id | Partially update an inventory count |
DELETE | /inventory-counts/:id | Delete an inventory count |
Query Parameters (GET list)
| Param | Type | Default | Description |
|---|---|---|---|
search | string | — | Case-insensitive search on locationName |
page | number | 1 | Page number |
size | number | 10 | Page size (0 = no limit) |
orderBy | string | — | Sort field: locationName, countDate, createdAt, status, countType |
order | string | asc | Sort 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 UUIDcreatedAt— server timestampcostDetail— computed cost snapshot with discrepancies
Database Schema
Table: inventory_count
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Auto-generated |
business_id | UUID (FK) | Multi-tenancy scope |
location_id | UUID (FK) | Location where count occurred |
location_name | VARCHAR | Denormalized location name |
count_date | TIMESTAMPTZ | Date of physical count |
count_type | VARCHAR | FULL or CYCLE |
status | VARCHAR | e.g., ACTIVE, INACTIVE |
detail | JSONB | Items array with quantities |
cost_detail | JSONB | Frozen cost snapshot with discrepancies |
total_amount | NUMERIC | Total counted amount |
total_base_amount | NUMERIC | Amount in base currency |
exchange_rate | NUMERIC(20,8) | FX rate to base currency |
currency_id | UUID (FK) | Currency reference |
variant_id | UUID (FK) | Optional product variant reference |
notes | TEXT | Free-text notes |
document_number | VARCHAR | Human-readable document number |
created_by | UUID (FK) | Creator reference |
created_at | TIMESTAMPTZ | Auto-generated |
updated_by | UUID (FK) | Last updater |
updated_at | TIMESTAMPTZ | Last update timestamp |
Design Decisions
- Cost snapshot at creation: Costs are frozen into
costDetailat creation time to prevent retroactive changes from affecting historical count accuracy. - Discrepancy calculation: The system calculates
expectedQuantityfrom current inventory anddiscrepancy = actual - expected, enabling variance reports without re-querying. - Event-driven side effects:
OnCreateInventoryCountEventis emitted after creation, allowing inventory adjustment handlers to react without coupling. - Token-based repository injection: Uses
INVENTORY_COUNTS_REPOSITORYsymbol for proper dependency inversion following hexagonal architecture.
Related Modules
- 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)