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:
@@ -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>
|
||||
Reference in New Issue
Block a user