Stock Count Module
Inventory cycle counting system with session-based workflows, scope-driven product selection, multi-strategy task generation, snapshot-based variance detection, and idempotent posting to inventory adjustments.
Architecture
stock-count/
├── stock-count.module.ts # NestJS module
├── domain/
│ └── stock-count-repository.domain.ts # Repository port (interface)
├── application/
│ └── stock-count.service.ts # Business logic / use cases
├── infrastructure/
│ └── stock-count.repository.ts # Kysely database adapter
└── interfaces/
├── stock-count.controller.ts # HTTP controller (18 endpoints)
├── dtos/ # Request DTOs (14 files)
└── query/ # Pagination query objects (3 files)
Dependencies: InventoryAdjustmentsModule, InventoryLedgersModule
Domain Concepts
Session Lifecycle
DRAFT → ACTIVE → CLOSED → APPROVED → POSTED
│ │ │
│ │ └──→ REJECTED
│ └──────────→ REJECTED
└───────────────────→ REJECTED
| Status | Description |
|---|---|
| DRAFT | Session created, scopes and tasks can be added |
| ACTIVE | Counting in progress, entries can be submitted |
| CLOSED | Counting complete, no more entries accepted |
| APPROVED | Reviewed and approved for posting |
| POSTED | Adjustments posted to inventory |
| REJECTED | Session rejected at any pre-posted stage |
Session Types
- CYCLE — Partial count targeting specific products/categories/zones
- FULL — Complete inventory count for a location
Scopes
Scopes define what to count. A session can have multiple scopes:
category— All products in a categorybrand— All products of a brandproduct— A specific productzone/bin— Products in a warehouse zone or binall— All products at the location
Task Generation Strategies
- by_zone_bin — One task per unique zone/bin combination from scopes
- by_category — One task per category scope, round-robin assigned
- by_assignee — One task per assignee rule
Snapshots
Point-in-time capture of on-hand quantities before counting begins. Handles:
- Tracked inventory (batch/serial) from
inventoryDetailtable - Residual inventory (difference between inventory total and detail sum)
- Untracked inventory (no batch/serial records)
Variance Detection
The stockCountVarianceV database view compares snapshot quantities against counted entries. Supports filtering by task and minimum variance threshold.
Posting (Idempotent)
Posting creates inventory adjustments for variances:
- Atomically claims the session (prevents concurrent double-posting)
- Groups items by direction (INCREASE / DECREASE)
- Creates one
InventoryAdjustmentper direction - Generates
InventoryLedgerentries viabuildLedgerRowsFromDetail() - Uses
idempotencyKeyfor safe retries
API Endpoints
Sessions
| Method | Path | Description |
|---|---|---|
POST | /stock-count/sessions | Create session (DRAFT) |
GET | /stock-count/sessions | List sessions (paginated) |
GET | /stock-count/sessions/:id | Get session by ID |
PATCH | /stock-count/sessions/:id/start | Start session (DRAFT → ACTIVE) |
PATCH | /stock-count/sessions/:id/close | Close session (ACTIVE → CLOSED) |
PATCH | /stock-count/sessions/:id/approve | Approve session (CLOSED → APPROVED) |
PATCH | /stock-count/sessions/:id/reject | Reject session |
POST | /stock-count/sessions/:id/snapshot | Create inventory snapshot |
GET | /stock-count/sessions/:id/snapshots | Get snapshots |
POST | /stock-count/sessions/:id/post | Post adjustments to inventory |
GET | /stock-count/sessions/:id/variance | Get variance report |
GET | /stock-count/sessions/:id/postings | Get post summary |
GET | /stock-count/sessions/:id/ledger-entries | Get ledger entries |
Scopes
| Method | Path | Description |
|---|---|---|
POST | /stock-count/sessions/:id/scopes | Add scope |
GET | /stock-count/sessions/:id/scopes | List scopes |
DELETE | /stock-count/sessions/:sessionId/scopes/:scopeId | Delete scope |
Tasks
| Method | Path | Description |
|---|---|---|
POST | /stock-count/sessions/:id/tasks/generate | Generate tasks |
GET | /stock-count/tasks | List tasks (paginated) |
GET | /stock-count/tasks/:id | Get task by ID |
PATCH | /stock-count/tasks/:id | Update task status |
PATCH | /stock-count/tasks/:id/approve | Approve task (RECONCILED) |
PATCH | /stock-count/tasks/:id/recount | Request recount (PAUSED) |
Entries
| Method | Path | Description |
|---|---|---|
POST | /stock-count/entries | Submit entries (bulk) |
GET | /stock-count/entries | List entries (paginated) |
GET | /stock-count/entries/:id | Get entry by ID |
PATCH | /stock-count/entries/:id | Update entry |
Example Workflow
1. POST /stock-count/sessions → Create DRAFT session
2. POST /stock-count/sessions/:id/scopes → Add scope (category: Beverages)
3. POST /stock-count/sessions/:id/tasks/generate → Generate tasks (by_category)
4. PATCH /stock-count/sessions/:id/start → Start session (ACTIVE)
5. POST /stock-count/sessions/:id/snapshot → Capture current on-hand
6. POST /stock-count/entries → Counter submits counts
7. GET /stock-count/sessions/:id/variance → Review variances
8. PATCH /stock-count/tasks/:taskId/approve → Approve task
9. PATCH /stock-count/sessions/:id/close → Close session
10. PATCH /stock-count/sessions/:id/approve → Approve session
11. POST /stock-count/sessions/:id/post → Post adjustments
Database Tables
stockCountSession— Main session containerstockCountScope— What products to countstockCountTask— Assigned counting tasksstockCountEntry— Individual count recordsstockCountSnapshot— Point-in-time on-hand capturestockCountVarianceV— View: snapshot vs. counted variancestockCountPostSummaryV— View: what adjustments will be created
Multi-Tenancy
All queries are scoped by businessId and locationId. Sessions inherit business/location context, and all child entities (scopes, tasks, entries) operate within that scope.