--- phase: 04-inventory-expenses-and-financial-reports plan: 02 type: execute wave: 2 depends_on: ["04-01"] files_modified: - prisma/schema.prisma - src/lib/services/asset-service.ts - src/lib/services/inventory-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 - src/lib/__tests__/asset-service.test.ts autonomous: true must_haves: truths: - "An asset can be assigned to a subscriber with condition tracking" - "An asset can be assigned to a technician for field work" - "An assigned asset shows full location history (chronological timeline)" - "Returning an asset records condition at return time" - "Disposal requires admin role — non-admin users get 403" - "Disposal creates a write-off JE (DR 5030 Equipment Expense, CR 1200 Equipment Inventory)" artifacts: - path: "src/lib/services/asset-service.ts" provides: "assignToSubscriber, assignToTechnician, returnAsset, disposeAsset, getAssetHistory" exports: ["AssetService"] - path: "src/lib/__tests__/asset-service.test.ts" provides: "Tests for assignment, return, disposal, history, authorization" min_lines: 80 key_links: - from: "src/lib/services/asset-service.ts" to: "src/lib/services/inventory-service.ts" via: "recordMovement for ISSUED/RETURNED/DISPOSED movements" pattern: "InventoryService\\.recordMovement" - from: "src/lib/services/asset-service.ts" to: "src/lib/accounting/journal-entry-service.ts" via: "JournalEntryService.createEntry for disposal write-off" pattern: "JournalEntryService\\.createEntry" --- Build asset lifecycle management on top of the inventory event-ledger: assign serialized items to subscribers and technicians, track condition at assignment and return, enforce admin-only disposal with write-off JE, and provide chronological location history. 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. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.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:** 1. Assign item to subscriber — creates ISSUED movement with SUBSCRIBER location 2. Assign item to technician — creates ISSUED movement with TECHNICIAN location 3. Return item from subscriber — creates RETURNED movement back to WAREHOUSE with condition 4. Return item from technician — creates RETURNED movement back to WAREHOUSE 5. Reject assignment of item already with a subscriber (must return first) 6. Reject assignment of batch item to subscriber (only SERIALIZED allowed) 7. Dispose item — ADMIN role creates DISPOSED movement + write-off JE (verify DR 5030, CR 1200) 8. Reject disposal by non-admin (OFFICE_STAFF user gets authorization error) 9. Reject disposal of item not in warehouse 10. Get asset history — returns chronological timeline with resolved location names 11. 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 - 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 After completion, create `.planning/phases/04-inventory-expenses-and-financial-reports/04-02-SUMMARY.md`