# 02 — Stock Ledger **Status**: Not Started **Depends On**: 01 — Item Catalog --- ## Goal Introduce stock-per-location as a first-class, transaction-driven concept. Covers [mainPlan.md §8](./mainPlan.md#8-enterprise-wide-inventory--stock) in full, plus the location-query half of §6 (`getStockByLocation`, `putawayStock`). Everything downstream — loading events, delivery/sale ride execution, reconciliation — reads or writes through this ledger. --- ## Background / Context Nothing in the current schema tracks quantity of an `Item` anywhere. `checkStockAvailability` (§1) and every warehouse function in §6 have no data source to query today. This step must land before [04_loading_event.md](./04_loading_event.md). --- ## Data Model Changes ### Stock Balance (per location) A denormalized running balance per `(ItemId, LocationId)` pair, kept in sync by the movement transaction table below — never edited directly by any service. ### Stock Movement Transaction One row per stock change: `ItemId`, `LocationId`, `Quantity` (signed: +load/return, -sale/damage), `MovementType` (`Load`, `Sale`, `Return`, `Damage`, `Adjustment`), `ReferenceId` (the order/ride/GRN that caused it), `CreatedAt`. This is the source of truth; the balance is its running sum. ### Location Abstraction (Warehouse vs Vehicle) A `Location` concept that can resolve to either a `Warehouse` or a `Vehicle` (once a ride is open — see [03_ride_lifecycle.md](./03_ride_lifecycle.md)), so the same movement/balance model serves both `getMultiLocationStock` (§8) and the vehicle-as-temporary-warehouse pattern. --- ## API Endpoints | Function reference | Endpoint intent | |---|---| | `getMultiLocationStock(sku)` (§8) | `GET /api/stock/items/{sku}` — balance across every location | | `getStockByLocation(warehouseId, sku)` (§6) | `GET /api/stock/locations/{locationId}/items/{sku}` | | `setReorderPoint(sku, warehouseId, threshold)` (§8) | `PUT /api/stock/locations/{locationId}/items/{sku}/reorder-point` | | `createStockTransfer(fromWarehouseId, toWarehouseId, items[])` (§8) | `POST /api/stock/transfers` | | `trackBatchExpiry(sku, batchNo, expiryDate)` (§8) | Deferred — see Open Questions | | `calculateStockValuation(warehouseId, method)` (§8) | `GET /api/stock/locations/{locationId}/valuation?method=fifo\|weighted-average` | --- ## Business Rules - Stock is never edited directly — every change is a `Stock Movement Transaction`; the balance is always the running sum, recalculated or cached but never hand-edited. - A movement cannot take a balance negative unless explicitly flagged as an `Adjustment` (physical count correction). - Every movement must carry a `ReferenceId` back to the business event that caused it — no freestanding movements outside of documented `Adjustment`s. --- ## Fraud / Control Considerations - `Adjustment` movements should be restricted to Supervisor level and above (see role-permission mapping in `Data/Seed/RoleAndPermissionSeed.cs`), and should require a reason code once [09_fraud_controls.md](./09_fraud_controls.md) lands. --- ## Open Questions - Is batch/expiry tracking (`trackBatchExpiry`) needed in v1, or can it be deferred until a regulated/perishable product line requires FEFO enforcement? - FIFO vs weighted-average valuation — does the business need both, or a single fixed method? --- ## Implementation Notes > Fill this section in during planning discussion before writing any code.