Files
kevin-asprec b5f2f3946b 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>
2026-03-05 10:00:55 +08:00

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
04-01
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
true
truths artifacts key_links
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)
path provides exports
src/lib/services/asset-service.ts assignToSubscriber, assignToTechnician, returnAsset, disposeAsset, getAssetHistory
AssetService
path provides min_lines
src/lib/__tests__/asset-service.test.ts Tests for assignment, return, disposal, history, authorization 80
from to via pattern
src/lib/services/asset-service.ts src/lib/services/inventory-service.ts recordMovement for ISSUED/RETURNED/DISPOSED movements InventoryService.recordMovement
from to via pattern
src/lib/services/asset-service.ts src/lib/accounting/journal-entry-service.ts JournalEntryService.createEntry for disposal write-off 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.

<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:

  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

<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>
After completion, create `.planning/phases/04-inventory-expenses-and-financial-reports/04-02-SUMMARY.md`