docs(04): create phase plan — Inventory, Expenses, and Financial Reports

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>
This commit is contained in:
kevin-asprec
2026-03-05 10:00:55 +08:00
parent d8bf8d52ac
commit b5f2f3946b
6 changed files with 1015 additions and 7 deletions

View File

@@ -0,0 +1,201 @@
---
phase: 04-inventory-expenses-and-financial-reports
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- prisma/schema.prisma
- src/lib/services/inventory-service.ts
- src/lib/accounting/chart-of-accounts.ts
- src/lib/tenant.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
autonomous: true
must_haves:
truths:
- "Staff can register a hardware item with type, model, serial number, purchase cost, purchase date, and warranty expiry"
- "Staff can record immutable stock movements (RECEIVED, ISSUED, RETURNED, DISPOSED, TRANSFERRED)"
- "Current stock levels are derived from movement history — no mutable quantity column"
- "Every RECEIVED movement posts a journal entry (DR 1200 Equipment Inventory, CR 2010 AP)"
- "Consumables (cables, connectors) are tracked by type+quantity batch; serialized items tracked individually"
artifacts:
- path: "prisma/schema.prisma"
provides: "InventoryItem, StockMovement, ItemType enums and models"
contains: "model InventoryItem"
- path: "src/lib/services/inventory-service.ts"
provides: "registerItem, recordMovement, getStockLevels, getItemMovements"
exports: ["InventoryService"]
- path: "src/lib/__tests__/inventory-service.test.ts"
provides: "Tests for registration, movements, stock derivation, JE posting"
min_lines: 100
key_links:
- from: "src/lib/services/inventory-service.ts"
to: "src/lib/accounting/journal-entry-service.ts"
via: "JournalEntryService.createEntry for RECEIVED movements"
pattern: "JournalEntryService\\.createEntry"
- from: "src/app/api/inventory/items/route.ts"
to: "src/lib/services/inventory-service.ts"
via: "API routes calling InventoryService methods"
pattern: "InventoryService\\."
---
<objective>
Build the inventory event-ledger foundation: hardware item registration with dual tracking (serialized + batch), immutable stock movement records, derived stock level computation, and automatic journal entry posting for receiving movements.
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.
</objective>
<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>
<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)
</context>
<tasks>
<task type="auto">
<name>Task 1: Schema — InventoryItem, StockMovement models and enums</name>
<files>prisma/schema.prisma</files>
<action>
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 quantity
- `ItemCondition`: `NEW`, `REFURBISHED`, `USED`, `DAMAGED` — condition at time of movement
- `MovementType`: `RECEIVED`, `ISSUED`, `RETURNED`, `DISPOSED`, `TRANSFERRED`
- `LocationType`: `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).
</action>
<verify>npx prisma validate passes with no errors</verify>
<done>InventoryItem and StockMovement models exist in schema with all fields, enums, indexes, and relations</done>
</task>
<task type="auto">
<name>Task 2: InventoryService, API routes, migration, and tests</name>
<files>
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
</files>
<action>
**InventoryService** (`src/lib/services/inventory-service.ts`):
- Static class following existing service patterns (see collector-service.ts, payment-service.ts)
- `registerItem(tenantPrisma, tenantId, data)` — creates InventoryItem. Validates: serialNumber required if trackingType=SERIALIZED, serialNumber must be null/undefined for BATCH. Returns created item.
- `recordMovement(tenantPrisma, tenantId, data)` — creates immutable StockMovement. Validates:
- RECEIVED: toLocationType required (must be WAREHOUSE), fromLocationType must be null
- ISSUED: fromLocationType+toLocationType required
- RETURNED: fromLocationType+toLocationType required, toLocationType must be WAREHOUSE
- DISPOSED: fromLocationType required, toLocationType must be null
- TRANSFERRED: both from+to required
- For SERIALIZED items: quantity must be 1
- For RECEIVED movements: auto-create JE via JournalEntryService.createEntry (DR 1200 Equipment Inventory, CR 2010 Accounts Payable) using purchaseCost or movement amount. Source=SYSTEM, referenceType="StockMovement".
- `getStockLevels(tenantPrisma, filters?)` — derives current stock by aggregating movements:
- RECEIVED/RETURNED add to stock at toLocation
- ISSUED/TRANSFERRED remove from fromLocation, add to toLocation
- DISPOSED removes from fromLocation
- Group by itemType and location. Return array of {itemId?, itemType, locationName, locationType, quantity}
- For serialized items, return individual item status (current location derived from latest movement)
- `getItemMovements(tenantPrisma, itemId)` — returns chronological movement history for an item
- `listItems(tenantPrisma, filters?)` — list items with optional filters (itemType, trackingType, isActive)
**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.
</action>
<verify>npx jest inventory-service --verbose passes all tests</verify>
<done>InventoryService handles registration, all 5 movement types, stock level derivation, and JE posting for RECEIVED. All API routes respond correctly. All tests pass.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<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>
<output>
After completion, create `.planning/phases/04-inventory-expenses-and-financial-reports/04-01-SUMMARY.md`
</output>