Phase 04: 5 plans in 2 waves - Wave 1: 04-01 (inventory event-ledger), 04-03 (expense tracking), 04-05 (financial reports) — parallel - Wave 2: 04-02 (asset management), 04-04 (expense reports + audit trail) — sequential - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-inventory-expenses-and-financial-reports | 01 | execute | 1 |
|
true |
|
Purpose: This is the core inventory data model that all asset management (04-02) builds on. The immutable movement ledger is a locked architectural decision — no mutable quantity columns. Output: InventoryItem/StockMovement schema, InventoryService with full CRUD+movements, API routes, and tests.
<execution_context> @C:\Users\KevinAsprec.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-inventory-expenses-and-financial-reports/04-CONTEXT.md @prisma/schema.prisma @src/lib/accounting/journal-entry-service.ts @src/lib/accounting/chart-of-accounts.ts @src/lib/tenant.ts @src/lib/services/collector-service.ts (reference for service pattern with JE posting) Task 1: Schema — InventoryItem, StockMovement models and enums prisma/schema.prisma Add the following enums and models to the Prisma schema:Enums:
ItemTrackingType:SERIALIZED,BATCH— serialized items have unique serial numbers; batch items are tracked by quantityItemCondition:NEW,REFURBISHED,USED,DAMAGED— condition at time of movementMovementType:RECEIVED,ISSUED,RETURNED,DISPOSED,TRANSFERREDLocationType:WAREHOUSE,TECHNICIAN,SUBSCRIBER— where the item is
InventoryItem model:
- id (UUID), tenantId (String), name (String), itemType (String — e.g., "Router", "ONU", "Cable", "Connector")
- model (String? — brand/model for serialized items), serialNumber (String? — null for batch items)
- trackingType (ItemTrackingType)
- purchaseCost (Decimal? @db.Decimal(10,2)), purchaseDate (DateTime?), warrantyExpiry (DateTime?)
- isActive (Boolean @default(true))
- createdAt, updatedAt
- Relation: movements StockMovement[]
- @@unique([tenantId, serialNumber]) — only enforced when serialNumber is not null (Prisma handles this: unique constraint on nullable field only applies to non-null values)
- @@index([tenantId]), @@index([tenantId, itemType]), @@index([tenantId, trackingType])
StockMovement model:
- id (UUID), tenantId (String)
- inventoryItemId (String) — FK to InventoryItem
- movementType (MovementType)
- quantity (Int @default(1)) — always 1 for serialized, variable for batch
- condition (ItemCondition? — condition at time of movement)
- fromLocationType (LocationType?), fromLocationId (String?) — null for RECEIVED
- toLocationType (LocationType?), toLocationId (String?) — null for DISPOSED
- notes (String?)
- journalEntryId (String?) — JE for RECEIVED movements
- performedById (String) — FK to User who recorded the movement
- performedBy relation to User
- createdAt DateTime @default(now()) — immutable, no updatedAt
- @@index([tenantId]), @@index([inventoryItemId]), @@index([tenantId, movementType])
Add recordedMovements StockMovement[] @relation("MovementPerformedBy") to User model.
Run npx prisma generate after schema changes. Do NOT run migrate yet (test will handle that).
npx prisma validate passes with no errors
InventoryItem and StockMovement models exist in schema with all fields, enums, indexes, and relations
API Routes:
POST /api/inventory/items— register new item (ADMIN, OFFICE_STAFF)GET /api/inventory/items— list items with filters (ADMIN, OFFICE_STAFF, TECHNICIAN)GET /api/inventory/items/[id]— get item detail (ADMIN, OFFICE_STAFF, TECHNICIAN)POST /api/inventory/items/[id]/movements— record movement (ADMIN, OFFICE_STAFF)GET /api/inventory/items/[id]/movements— get item movement history (ADMIN, OFFICE_STAFF, TECHNICIAN)GET /api/inventory/stock-levels— get derived stock levels (ADMIN, OFFICE_STAFF)
All routes use withPermission() HOF pattern. Follow existing route patterns (e.g., collections route.ts).
Migration:
Apply migration using the Docker exec psql + prisma migrate resolve --applied pattern established in prior phases. Migration name: add_inventory_models.
Tests (src/lib/__tests__/inventory-service.test.ts):
- Register serialized item (with serial number)
- Register batch item (without serial number)
- Reject serialized item without serial number
- Record RECEIVED movement creates StockMovement + JE (verify JE: DR 1200, CR 2010)
- Record ISSUED movement (warehouse to technician)
- Record RETURNED movement (technician to warehouse)
- Record DISPOSED movement
- Derive stock levels from movement history (receive 10, issue 3 = 7 in warehouse)
- Serialized item: derive current location from latest movement
- Get movement history returns chronological order
Follow existing test patterns: createTenant for setup, explicit cleanup order in afterAll. Cleanup order: stockMovements -> inventoryItems -> journalEntryLines -> null reversesEntryId -> journalEntries -> accountingPeriods -> accounts -> users -> tenant. npx jest inventory-service --verbose passes all tests InventoryService handles registration, all 5 movement types, stock level derivation, and JE posting for RECEIVED. All API routes respond correctly. All tests pass.
- `npx prisma validate` passes - `npx jest inventory-service --verbose` — all tests pass - Stock levels are derived (no quantity column on InventoryItem) - RECEIVED movement creates balanced JE (DR 1200, CR 2010) - Serialized items enforce serial number uniqueness per tenant<success_criteria>
- Staff can register hardware items (serialized with serial number, batch without)
- All 5 movement types (RECEIVED, ISSUED, RETURNED, DISPOSED, TRANSFERRED) create immutable records
- Stock levels derived from movement aggregation — no mutable quantity column exists
- RECEIVED movements auto-post journal entries to the ledger
- All tests pass </success_criteria>