docs(04-01): complete inventory event-ledger plan
Tasks completed: 2/2 - Task 1: Schema — InventoryItem, StockMovement models and enums - Task 2: InventoryService, API routes, migration, and tests (13 passing) SUMMARY: .planning/phases/04-inventory-expenses-and-financial-reports/04-01-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,19 +9,19 @@ See: .planning/PROJECT.md (updated 2026-03-04)
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 3 of 5 (Operational Modules) — COMPLETE
|
Phase: 4 of 5 (Inventory, Expenses, and Financial Reports)
|
||||||
Plan: 5 of 5 in phase 3 (17/20 total complete)
|
Plan: 1 of 3 in phase 4 (18/20 total complete)
|
||||||
Status: Phase 3 complete. 03-01 (Zones), 03-02 (Collections/Remittances), 03-03 (Ticketing), 03-04 (Job Orders), 03-05 (Technician Compensation) all complete.
|
Status: In progress. 04-01 (Inventory Event-Ledger) complete.
|
||||||
Last activity: 2026-03-05 — Completed 03-05-PLAN.md (Technician Management — 27 tests, hybrid compensation, PER_JOB/SALARY/HYBRID models)
|
Last activity: 2026-03-05 — Completed 04-01-PLAN.md (Inventory Event-Ledger — 13 tests, dual tracking, immutable movements, JE posting)
|
||||||
|
|
||||||
Progress: [█████████████████░░░] 85% (17/20 plans across all phases)
|
Progress: [██████████████████░░] 90% (18/20 plans across all phases)
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
**Velocity:**
|
||||||
- Total plans completed: 11
|
- Total plans completed: 12
|
||||||
- Average duration: 9.5 min
|
- Average duration: 10 min
|
||||||
- Total execution time: 104 min
|
- Total execution time: 120 min
|
||||||
|
|
||||||
**By Phase:**
|
**By Phase:**
|
||||||
|
|
||||||
@@ -30,10 +30,11 @@ Progress: [█████████████████░░░] 85% (17
|
|||||||
| 01-foundation | 5/5 complete | 41 min | 8.2 min |
|
| 01-foundation | 5/5 complete | 41 min | 8.2 min |
|
||||||
| 02-subscriber-and-billing-core | 5/5 complete | 57 min | 11.4 min |
|
| 02-subscriber-and-billing-core | 5/5 complete | 57 min | 11.4 min |
|
||||||
| 03-operational-modules | 5/5 complete | ~65 min | ~13 min |
|
| 03-operational-modules | 5/5 complete | ~65 min | ~13 min |
|
||||||
|
| 04-inventory-expenses-reports | 1/3 complete | 16 min | 16 min |
|
||||||
|
|
||||||
**Recent Trend:**
|
**Recent Trend:**
|
||||||
- Last 10 plans: 01-05 (6 min), 02-01 (7 min), 02-03 (7 min), 02-02 (16 min), 02-04 (12 min), 02-05 (8 min), 03-01 (15 min), 03-02 (11 min), 03-04 (6 min), 03-05 (9 min)
|
- Last 10 plans: 02-01 (7 min), 02-03 (7 min), 02-02 (16 min), 02-04 (12 min), 02-05 (8 min), 03-01 (15 min), 03-02 (11 min), 03-04 (6 min), 03-05 (9 min), 04-01 (16 min)
|
||||||
- Trend: stable — 03-05 migration applied cleanly, all 27 tests passed on first run
|
- Trend: stable — 04-01 migration applied cleanly, all 13 tests passed on first run
|
||||||
|
|
||||||
*Updated after each plan completion*
|
*Updated after each plan completion*
|
||||||
|
|
||||||
@@ -119,6 +120,10 @@ Recent decisions affecting current work:
|
|||||||
- [03-05]: Missing job type rate defaults to 0 bonus — rateMap.get(jobType) ?? Decimal(0); not an error per design spec
|
- [03-05]: Missing job type rate defaults to 0 bonus — rateMap.get(jobType) ?? Decimal(0); not an error per design spec
|
||||||
- [03-05]: SALARY model detail returns jobs with rate=0 — consistent API shape across all 3 models; all detail responses have a jobs array
|
- [03-05]: SALARY model detail returns jobs with rate=0 — consistent API shape across all 3 models; all detail responses have a jobs array
|
||||||
- [03-05]: Compensation cleanup order in tests: jobOrders -> tickets -> ticketCategories -> jobTypeRates -> technicianProfiles -> zoneAssignments -> subscribers -> zones -> servicePlans -> users -> tenant
|
- [03-05]: Compensation cleanup order in tests: jobOrders -> tickets -> ticketCategories -> jobTypeRates -> technicianProfiles -> zoneAssignments -> subscribers -> zones -> servicePlans -> users -> tenant
|
||||||
|
- [04-01]: Only RECEIVED movements auto-post JEs (DR 1200, CR 2010) — ISSUED/RETURNED/DISPOSED/TRANSFERRED do not create JEs
|
||||||
|
- [04-01]: CASL subject is "Inventory" (existing type in types.ts) — OFFICE_STAFF gets can("manage", "Inventory")
|
||||||
|
- [04-01]: Stock levels derived in JS from movement aggregation — acceptable for ISP scale (same pattern as collector balances, outstanding reports)
|
||||||
|
- [04-01]: Inventory cleanup order in tests: stockMovements -> inventoryItems -> journalEntryLines -> null reversesEntryId -> journalEntries -> accountingPeriods -> accounts -> users -> tenant
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -136,6 +141,6 @@ None.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-05T00:17:08Z
|
Last session: 2026-03-05T02:32:00Z
|
||||||
Stopped at: Completed 03-05-PLAN.md (Technician Management — 27 tests, hybrid compensation, Phase 3 complete)
|
Stopped at: Completed 04-01-PLAN.md (Inventory Event-Ledger — 13 tests, dual tracking, immutable movements)
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
phase: 04-inventory-expenses-and-financial-reports
|
||||||
|
plan: 01
|
||||||
|
subsystem: inventory
|
||||||
|
tags: [inventory, stock-movements, event-ledger, journal-entries, dual-tracking]
|
||||||
|
dependency-graph:
|
||||||
|
requires: [02-01, 02-02]
|
||||||
|
provides: [InventoryItem-model, StockMovement-model, InventoryService, inventory-api-routes]
|
||||||
|
affects: [04-02]
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [immutable-event-ledger, derived-stock-levels, dual-tracking-serialized-batch]
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- prisma/migrations/20260305021700_add_inventory_models/migration.sql
|
||||||
|
- src/lib/services/inventory-service.ts
|
||||||
|
- src/app/api/inventory/items/route.ts
|
||||||
|
- src/app/api/inventory/items/[id]/route.ts
|
||||||
|
- src/app/api/inventory/items/[id]/movements/route.ts
|
||||||
|
- src/app/api/inventory/stock-levels/route.ts
|
||||||
|
- src/lib/__tests__/inventory-service.test.ts
|
||||||
|
modified:
|
||||||
|
- prisma/schema.prisma
|
||||||
|
- src/lib/casl/permissions.ts
|
||||||
|
decisions:
|
||||||
|
- id: inv-tracking-type
|
||||||
|
summary: "Dual tracking: SERIALIZED (serial number required, qty=1) vs BATCH (no serial, variable qty)"
|
||||||
|
- id: inv-je-received-only
|
||||||
|
summary: "Only RECEIVED movements auto-post JEs (DR 1200, CR 2010); ISSUED/RETURNED/DISPOSED/TRANSFERRED do not"
|
||||||
|
- id: inv-casl-subject
|
||||||
|
summary: "CASL subject is 'Inventory' (existing type), not 'InventoryItem' — OFFICE_STAFF gets manage Inventory"
|
||||||
|
metrics:
|
||||||
|
duration: "16 min"
|
||||||
|
completed: "2026-03-05"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 4 Plan 1: Inventory Event-Ledger Foundation Summary
|
||||||
|
|
||||||
|
Immutable stock movement ledger with dual tracking (serialized by serial number, batch by quantity), derived stock level computation, and automatic JE posting for receiving movements.
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
### Schema (Task 1)
|
||||||
|
- 4 new enums: `ItemTrackingType` (SERIALIZED/BATCH), `ItemCondition` (NEW/REFURBISHED/USED/DAMAGED), `MovementType` (RECEIVED/ISSUED/RETURNED/DISPOSED/TRANSFERRED), `LocationType` (WAREHOUSE/TECHNICIAN/SUBSCRIBER)
|
||||||
|
- `InventoryItem` model: dual tracking, serial number uniqueness per tenant (nullable unique), purchaseCost/purchaseDate/warrantyExpiry
|
||||||
|
- `StockMovement` model: immutable (no updatedAt), journalEntryId for RECEIVED, performedBy relation to User
|
||||||
|
- Indexes on tenantId, itemType, trackingType, inventoryItemId, movementType
|
||||||
|
|
||||||
|
### InventoryService (Task 2)
|
||||||
|
- `registerItem`: creates items, validates SERIALIZED requires serial number, BATCH rejects serial number
|
||||||
|
- `recordMovement`: validates per-movement-type rules, enforces qty=1 for SERIALIZED, auto-posts JE for RECEIVED (DR 1200 Equipment Inventory, CR 2010 Accounts Payable)
|
||||||
|
- `getStockLevels`: derives stock from movement aggregation (RECEIVED/RETURNED add, ISSUED/TRANSFERRED remove+add, DISPOSED remove), filters zero-quantity entries
|
||||||
|
- `getItemMovements`: chronological history with performedBy details
|
||||||
|
- `listItems`: filterable by itemType, trackingType, isActive
|
||||||
|
- `getItem`: single item with recent movements
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
- `POST /api/inventory/items` — register new item (ADMIN, OFFICE_STAFF)
|
||||||
|
- `GET /api/inventory/items` — list items with filters
|
||||||
|
- `GET /api/inventory/items/[id]` — item detail
|
||||||
|
- `POST /api/inventory/items/[id]/movements` — record movement
|
||||||
|
- `GET /api/inventory/items/[id]/movements` — movement history
|
||||||
|
- `GET /api/inventory/stock-levels` — derived stock levels
|
||||||
|
|
||||||
|
### CASL Permissions
|
||||||
|
- OFFICE_STAFF: `can("manage", "Inventory")` added
|
||||||
|
- TECHNICIAN: existing `can("read", "Inventory")` preserved
|
||||||
|
|
||||||
|
### Tests (13 passing)
|
||||||
|
- Registration: serialized with serial, batch without, reject serialized without serial, reject batch with serial
|
||||||
|
- Movements: RECEIVED with JE (DR 1200, CR 2010), ISSUED, RETURNED, DISPOSED, reject SERIALIZED qty>1
|
||||||
|
- Stock derivation: batch receive 10 issue 3 = 7, serialized latest location
|
||||||
|
- History: chronological order
|
||||||
|
- List: itemType filter
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Only RECEIVED movements create JEs | Other movement types (ISSUED, RETURNED, etc.) are internal transfers that don't affect AP. Asset reclassification JEs for ISSUED/DISPOSED will be added in 04-02 if needed |
|
||||||
|
| CASL subject "Inventory" not "InventoryItem" | Reuses existing subject type from types.ts. OFFICE_STAFF gets full manage access |
|
||||||
|
| Stock levels computed in JS not SQL | Follows existing pattern (collector balances, outstanding reports). Acceptable for ISP scale |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 2 - Missing Critical] OFFICE_STAFF inventory CASL permission**
|
||||||
|
- **Found during:** Task 2 (API route creation)
|
||||||
|
- **Issue:** OFFICE_STAFF had no Inventory permissions in CASL, would fail withPermission() checks
|
||||||
|
- **Fix:** Added `can("manage", "Inventory")` to OFFICE_STAFF role
|
||||||
|
- **Files modified:** src/lib/casl/permissions.ts
|
||||||
|
- **Commit:** a742f70
|
||||||
|
|
||||||
|
**2. [Rule 3 - Blocking] CASL subject mismatch**
|
||||||
|
- **Found during:** Task 2 (API route creation)
|
||||||
|
- **Issue:** Plan specified "InventoryItem" as withPermission subject but types.ts only has "Inventory"
|
||||||
|
- **Fix:** Used "Inventory" subject in all API routes
|
||||||
|
- **Files modified:** All API route files
|
||||||
|
- **Commit:** a742f70
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
04-02 (Asset Lifecycle) can build on:
|
||||||
|
- InventoryItem and StockMovement models are stable
|
||||||
|
- InventoryService.recordMovement handles all 5 movement types
|
||||||
|
- Stock level derivation ready for assignment tracking (ISSUED to subscriber)
|
||||||
|
- JE posting pattern established (can extend for disposal write-offs)
|
||||||
|
|
||||||
|
### Cleanup Order for Tests
|
||||||
|
stockMovements -> inventoryItems -> journalEntryLines -> null reversesEntryId -> journalEntries -> accountingPeriods -> accounts -> users -> tenant
|
||||||
Reference in New Issue
Block a user