Files
kevin-asprec daf5bddf0b 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>
2026-03-05 10:33:18 +08:00

5.4 KiB

phase, plan, subsystem, tags, dependency-graph, tech-stack, key-files, decisions, metrics
phase plan subsystem tags dependency-graph tech-stack key-files decisions metrics
04-inventory-expenses-and-financial-reports 01 inventory
inventory
stock-movements
event-ledger
journal-entries
dual-tracking
requires provides affects
02-01
02-02
InventoryItem-model
StockMovement-model
InventoryService
inventory-api-routes
04-02
added patterns
immutable-event-ledger
derived-stock-levels
dual-tracking-serialized-batch
created modified
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
prisma/schema.prisma
src/lib/casl/permissions.ts
id summary
inv-tracking-type Dual tracking: SERIALIZED (serial number required, qty=1) vs BATCH (no serial, variable qty)
id summary
inv-je-received-only Only RECEIVED movements auto-post JEs (DR 1200, CR 2010); ISSUED/RETURNED/DISPOSED/TRANSFERRED do not
id summary
inv-casl-subject CASL subject is 'Inventory' (existing type), not 'InventoryItem' — OFFICE_STAFF gets manage Inventory
duration completed
16 min 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