Files
NetForge/.planning/phases/03-operational-modules/03-04-PLAN.md
kevin-asprec d54b517e2e docs(03): create phase plan
Phase 03: Operational Modules
- 5 plans in 3 waves
- Wave 1: 03-01 (zones), 03-03 (tickets) — parallel
- Wave 2: 03-02 (collector collections), 03-04 (job orders) — parallel
- Wave 3: 03-05 (technician compensation)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:11:44 +08:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
03-operational-modules 04 execute 2
03-03
prisma/schema.prisma
src/lib/prisma-tenant.ts
src/lib/services/job-order-service.ts
src/lib/services/ticket-service.ts
src/app/api/tickets/[id]/job-orders/route.ts
src/app/api/job-orders/route.ts
src/app/api/job-orders/[id]/route.ts
src/app/api/job-orders/[id]/status/route.ts
src/lib/__tests__/job-order-service.test.ts
true
truths artifacts key_links
Staff can convert a ticket into a job order assigned to a technician
One ticket can have multiple job orders (1:many)
Job orders follow lifecycle: PENDING -> IN_PROGRESS -> COMPLETED (or CANCELLED)
Technician can update status of their own assigned job orders
When ALL non-cancelled job orders on a ticket are COMPLETED, ticket auto-resolves
When ALL job orders on a ticket are CANCELLED, ticket reverts to OPEN
Creating first job order on OPEN ticket transitions ticket to ASSIGNED
Job completion includes outcome notes and completion date
path provides contains
prisma/schema.prisma JobOrder model with status enum and ticket relation model JobOrder
path provides exports
src/lib/services/job-order-service.ts Job order CRUD, status transitions, ticket synchronization
createJobOrder
updateJobOrderStatus
getJobOrder
listJobOrders
path provides contains
src/lib/services/ticket-service.ts Updated with resolveTicket and revertToOpen calls from job order service resolveTicket
path provides min_lines
src/lib/__tests__/job-order-service.test.ts Integration tests for job order lifecycle and ticket sync 150
from to via pattern
src/lib/services/job-order-service.ts src/lib/services/ticket-service.ts checkTicketAutoResolve and checkTicketRevertToOpen after status changes resolveTicket|transitionTicketStatus
from to via pattern
src/app/api/job-orders/[id]/status/route.ts src/lib/services/job-order-service.ts updateJobOrderStatus with technician self-service updateJobOrderStatus
Create the job order workflow: JobOrder model linked to Ticket (1:many), job order lifecycle (PENDING -> IN_PROGRESS -> COMPLETED/CANCELLED), technician assignment, ticket-job status synchronization (auto-resolve on all complete, revert to OPEN on all cancelled), and technician self-service status updates.

Purpose: Job orders are the execution units that technicians work on. The bidirectional sync with tickets ensures the ticket lifecycle stays accurate as work progresses. Output: JobOrder model, job-order-service.ts, updated ticket-service.ts, 4 API routes, integration 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/03-operational-modules/03-CONTEXT.md @.planning/phases/03-operational-modules/03-RESEARCH.md @.planning/phases/03-operational-modules/03-03-SUMMARY.md @prisma/schema.prisma @src/lib/prisma-tenant.ts @src/lib/services/ticket-service.ts @src/lib/casl/permissions.ts @src/lib/__tests__/payment.test.ts (test pattern reference) Task 1: JobOrder schema, migration, and tenant scoping prisma/schema.prisma src/lib/prisma-tenant.ts 1. Add enum to schema.prisma: - `enum JobOrderStatus { PENDING IN_PROGRESS COMPLETED CANCELLED }`
  1. Add JobOrder model:

    • id (uuid), tenantId
    • orderNumber (String) — auto-generated JO-NNNN
    • ticketId (String, FK to Ticket)
    • jobType (String) — e.g., "Installation", "Repair", "Maintenance" (free-form, matches JobTypeRate in 03-05)
    • description (String?) — specific instructions for this job
    • assignedToId (String, FK to User — the technician)
    • status (JobOrderStatus, default PENDING)
    • scheduledDate (DateTime?) — when the job is scheduled
    • startedAt (DateTime?) — when technician started work
    • completedAt (DateTime?) — when job was completed
    • outcomeNotes (String?) — technician's completion notes
    • cancelledAt (DateTime?), cancelReason (String?)
    • createdById (String, FK to User — staff who created the job order)
    • createdAt, updatedAt
    • Relations: ticket -> Ticket, assignedTo -> User, createdBy -> User
    • @@unique([tenantId, orderNumber])
    • @@index([tenantId]), @@index([tenantId, ticketId]), @@index([tenantId, assignedToId]), @@index([tenantId, status])
  2. Add reverse relations:

    • Ticket: jobOrders JobOrder[]
    • User: assignedJobOrders JobOrder[] @relation("JobOrderAssignedTo"), createdJobOrders JobOrder[] @relation("JobOrderCreatedBy")
  3. Run npx prisma migrate dev --name add-job-orders

  4. Add JobOrder to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension block.

    • npx prisma migrate dev succeeds
    • npx tsc --noEmit passes
    • Grep prisma-tenant.ts confirms "jobOrder" in TENANT_SCOPED_MODELS JobOrder model exists with ticket relation, migration applied, tenant scoping configured.
Task 2: Job order service, ticket sync, API routes, and integration tests src/lib/services/job-order-service.ts src/lib/services/ticket-service.ts src/app/api/tickets/[id]/job-orders/route.ts src/app/api/job-orders/route.ts src/app/api/job-orders/[id]/route.ts src/app/api/job-orders/[id]/status/route.ts src/lib/__tests__/job-order-service.test.ts 1. Update src/lib/services/ticket-service.ts — add/export two helper functions: - `checkTicketAutoResolve(tenantPrisma, ticketId)`: Query all job orders for ticket where status != CANCELLED. If count > 0 AND all have status COMPLETED, call resolveTicket (which is already idempotent). If count == 0 (all cancelled), do NOT auto-resolve. - `checkTicketRevertToOpen(tenantPrisma, ticketId)`: Query all job orders for ticket. If ALL are CANCELLED (none pending/in-progress/completed), and ticket.status is ASSIGNED, transition ticket to OPEN. If ticket is already OPEN or has non-cancelled job orders, no-op.
  1. Create src/lib/services/job-order-service.ts:

    • Sequential number generation: generateOrderNumber(tenantPrisma) — JO-NNNN pattern (same as ticket numbering).
    • createJobOrder(tenantPrisma, tenantId, { ticketId, jobType, description?, assignedToId, scheduledDate?, createdById }): a. Validate ticket exists and is not CLOSED b. Validate assignedToId user has TECHNICIAN role c. Generate orderNumber d. Create JobOrder with status PENDING e. If ticket status is OPEN, auto-transition to ASSIGNED via transitionTicketStatus f. Return job order
    • updateJobOrderStatus(tenantPrisma, tenantId, jobOrderId, { status, outcomeNotes?, cancelReason? }): Define VALID_JO_TRANSITIONS: PENDING -> [IN_PROGRESS, CANCELLED] IN_PROGRESS -> [COMPLETED, CANCELLED] COMPLETED -> [] (terminal) CANCELLED -> [] (terminal) a. Validate transition b. Update status with timestamps:
      • IN_PROGRESS: set startedAt
      • COMPLETED: set completedAt, require outcomeNotes (throw if missing)
      • CANCELLED: set cancelledAt, cancelReason optional c. After COMPLETED: call checkTicketAutoResolve(tenantPrisma, ticketId) d. After CANCELLED: call checkTicketRevertToOpen(tenantPrisma, ticketId)
    • getJobOrder(tenantPrisma, jobOrderId) — include ticket, assignedTo, createdBy
    • listJobOrders(tenantPrisma, { ticketId?, assignedToId?, status?, page?, limit? }) — filtered list
    • getMyJobOrders(tenantPrisma, technicianUserId, { status?, page?, limit? }) — convenience for technician self-service
  2. Create API routes:

    • POST /api/tickets/[id]/job-orders: withPermission("create", "JobOrder") -> createJobOrder (staff creates from ticket context; dynamic route pattern: ticketId from params)
    • GET /api/job-orders: withPermission("read", "JobOrder") -> listJobOrders with query filters. For TECHNICIAN role: auto-filter to assignedToId = user.id
    • GET /api/job-orders/[id]: withPermission("read", "JobOrder") -> getJobOrder
    • PUT /api/job-orders/[id]: withPermission("update", "JobOrder") -> update job order metadata (description, scheduledDate)
    • POST /api/job-orders/[id]/status: withPermission("update", "JobOrder") -> updateJobOrderStatus (body: { status, outcomeNotes?, cancelReason? })
  3. Create src/lib/tests/job-order-service.test.ts:

    • Setup: create tenant (auto-seeds categories), admin user, technician user (TECHNICIAN role), create subscriber, create ticket
    • Test: createJobOrder succeeds, returns JO-0001
    • Test: createJobOrder auto-transitions OPEN ticket to ASSIGNED
    • Test: second job order gets JO-0002, ticket stays ASSIGNED
    • Test: createJobOrder rejects non-TECHNICIAN assignee
    • Test: createJobOrder rejects CLOSED ticket
    • Test: updateJobOrderStatus PENDING -> IN_PROGRESS succeeds (sets startedAt)
    • Test: updateJobOrderStatus IN_PROGRESS -> COMPLETED succeeds (sets completedAt, requires outcomeNotes)
    • Test: COMPLETED without outcomeNotes throws
    • Test: invalid transition (COMPLETED -> IN_PROGRESS) throws
    • Test: auto-resolve: create 2 job orders on ticket, complete both -> ticket auto-resolves to RESOLVED
    • Test: revert-to-open: create 1 job order, cancel it -> ticket reverts from ASSIGNED to OPEN
    • Test: partial completion: 2 job orders, complete 1, cancel 1 -> ticket auto-resolves (all non-cancelled are completed)
    • Test: all cancelled with none completed -> ticket reverts to OPEN, does NOT resolve
    • Test: getMyJobOrders returns only technician's assigned orders
    • Test: cross-tenant isolation
    • Cleanup: jobOrders -> tickets -> ticketCategories -> subscribers -> servicePlans -> users -> tenant
    • npx vitest run src/lib/__tests__/job-order-service.test.ts — all tests pass
    • npx tsc --noEmit passes Job orders can be created from tickets, assigned to technicians, progressed through lifecycle, auto-resolve and revert-to-open ticket sync works correctly, technicians can self-service their assigned orders, all tests pass.
- `npx prisma migrate dev` succeeds - `npx tsc --noEmit` passes - `npx vitest run src/lib/__tests__/job-order-service.test.ts` — all green - Ticket auto-resolve works when all non-cancelled jobs complete - Ticket revert-to-open works when all jobs cancelled - Technician can only see/update their own job orders

<success_criteria>

  • JobOrder model with 1:many ticket relation
  • Sequential numbering (JO-NNNN)
  • Status transitions enforced by guard map
  • Ticket auto-transitions: OPEN -> ASSIGNED on first job, auto-resolve on all complete, revert to OPEN on all cancelled
  • Technicians can update their assigned job orders
  • Completion requires outcome notes
  • Cross-tenant isolation verified
  • All integration tests pass </success_criteria>
After completion, create `.planning/phases/03-operational-modules/03-04-SUMMARY.md`