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>
202 lines
10 KiB
Markdown
202 lines
10 KiB
Markdown
---
|
|
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>
|