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>
8.9 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 | 02 | execute | 2 |
|
|
true |
|
Purpose: Enables ISP staff to track where every piece of equipment is — from warehouse to technician to subscriber and back. The location history is the key differentiator for accountability. Output: AssetService with assign/return/dispose/history, 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 @.planning/phases/04-inventory-expenses-and-financial-reports/04-01-SUMMARY.md @prisma/schema.prisma @src/lib/services/inventory-service.ts @src/lib/accounting/journal-entry-service.ts Task 1: AssetService — assignment, return, disposal, and history src/lib/services/asset-service.ts src/app/api/inventory/items/[id]/assign/route.ts src/app/api/inventory/items/[id]/return/route.ts src/app/api/inventory/items/[id]/dispose/route.ts src/app/api/inventory/items/[id]/history/route.ts **AssetService** (`src/lib/services/asset-service.ts`): Static class building on InventoryService.recordMovement:-
assignToSubscriber(tenantPrisma, tenantId, { itemId, subscriberId, condition, performedById, notes? }):- Validate item exists and is SERIALIZED (batch items cannot be individually assigned to subscribers)
- Determine current location from latest movement (must be in WAREHOUSE or with a TECHNICIAN — cannot assign from subscriber to subscriber)
- Call InventoryService.recordMovement with movementType=ISSUED, fromLocation=current location, toLocationType=SUBSCRIBER, toLocationId=subscriberId, condition
- Return the created movement
-
assignToTechnician(tenantPrisma, tenantId, { itemId, technicianUserId, condition, performedById, notes? }):- Validate item is SERIALIZED
- Current location must be WAREHOUSE
- Call InventoryService.recordMovement with movementType=ISSUED, fromLocationType=WAREHOUSE, toLocationType=TECHNICIAN, toLocationId=technicianUserId, condition
-
returnAsset(tenantPrisma, tenantId, { itemId, condition, performedById, notes? }):- Validate item is SERIALIZED
- Current location must be with SUBSCRIBER or TECHNICIAN (not already in warehouse)
- Call InventoryService.recordMovement with movementType=RETURNED, from=current location, toLocationType=WAREHOUSE, condition (captures condition at return — NEW, USED, DAMAGED, REFURBISHED)
-
disposeAsset(tenantPrisma, tenantId, { itemId, performedById, notes?, userRoles }):- Validate item is SERIALIZED
- Validate userRoles includes ADMIN — disposal requires admin approval per CONTEXT.md
- Current location must be WAREHOUSE (cannot dispose from field)
- Create write-off JE: DR 5030 Equipment Expense, CR 1200 Equipment Inventory for the item's purchaseCost. Source=SYSTEM, referenceType="StockMovement", description="Disposal write-off: {item.name} SN:{item.serialNumber}"
- Call InventoryService.recordMovement with movementType=DISPOSED, fromLocationType=WAREHOUSE, journalEntryId from JE
-
getAssetHistory(tenantPrisma, itemId):- Fetch item with all movements ordered by createdAt ASC
- Return formatted timeline: each entry has { movementType, date, fromLocation (type+name), toLocation (type+name), condition, performedBy (user name), notes }
- Resolve location names: WAREHOUSE="Warehouse", SUBSCRIBER=subscriber name, TECHNICIAN=user name
-
Helper:
getCurrentLocation(tenantPrisma, itemId)— returns { locationType, locationId } from latest movement's to-fields (or null if disposed)
API Routes:
POST /api/inventory/items/[id]/assign— body: { assigneeType: "SUBSCRIBER"|"TECHNICIAN", assigneeId, condition, notes? }. ADMIN, OFFICE_STAFF.POST /api/inventory/items/[id]/return— body: { condition, notes? }. ADMIN, OFFICE_STAFF.POST /api/inventory/items/[id]/dispose— body: { notes? }. ADMIN only.GET /api/inventory/items/[id]/history— returns chronological timeline. ADMIN, OFFICE_STAFF, TECHNICIAN.
Use withPermission() HOF and dynamic route handler pattern from prior phases. npx prisma validate passes; API route files exist and export correct HTTP methods AssetService handles subscriber/technician assignment, return with condition, admin-only disposal with write-off JE, and chronological history
Task 2: Asset service tests src/lib/__tests__/asset-service.test.ts Write comprehensive tests for AssetService:Setup: createTenant, create admin user + office_staff user + technician user, create subscriber, create serialized InventoryItem, record initial RECEIVED movement (so item is in warehouse).
Test cases:
- Assign item to subscriber — creates ISSUED movement with SUBSCRIBER location
- Assign item to technician — creates ISSUED movement with TECHNICIAN location
- Return item from subscriber — creates RETURNED movement back to WAREHOUSE with condition
- Return item from technician — creates RETURNED movement back to WAREHOUSE
- Reject assignment of item already with a subscriber (must return first)
- Reject assignment of batch item to subscriber (only SERIALIZED allowed)
- Dispose item — ADMIN role creates DISPOSED movement + write-off JE (verify DR 5030, CR 1200)
- Reject disposal by non-admin (OFFICE_STAFF user gets authorization error)
- Reject disposal of item not in warehouse
- Get asset history — returns chronological timeline with resolved location names
- Full lifecycle: RECEIVED -> ISSUED to tech -> RETURNED -> ISSUED to subscriber -> RETURNED -> DISPOSED — history shows all 6 entries
Cleanup order: stockMovements -> inventoryItems -> journalEntryLines -> null reversesEntryId -> journalEntries -> subscribers -> servicePlans -> accountingPeriods -> accounts -> users -> tenant npx jest asset-service --verbose passes all tests All 11 test cases pass covering assignment, return, disposal authorization, history, and full lifecycle
- `npx jest asset-service --verbose` — all tests pass - Subscriber assignment creates correct movement record - Technician assignment creates correct movement record - Disposal enforces admin-only and creates write-off JE - History returns chronological timeline with location names<success_criteria>
- Assets can be assigned to subscribers and technicians
- Condition is captured at assignment and return
- Disposal requires admin role and posts write-off JE (DR 5030, CR 1200)
- Full location history available as chronological timeline
- All tests pass </success_criteria>