--- phase: 03-operational-modules plan: "04" type: execute wave: 2 depends_on: ["03-03"] files_modified: - prisma/schema.prisma - src/lib/prisma-tenant.ts - src/lib/services/job-order-service.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/app/api/tickets/[id]/job-orders/route.ts - src/lib/__tests__/job-order-service.test.ts autonomous: true must_haves: truths: - "Staff can convert a ticket into a job order assigned to a technician" - "One ticket can have multiple job orders (1:many)" - "Technician can view their assigned job orders and update status (PENDING -> IN_PROGRESS -> COMPLETED)" - "Job completion includes outcome notes, completion date" - "When ALL job orders on a ticket are completed, ticket auto-moves to RESOLVED" - "Staff manually closes ticket after confirming resolution (two-step: auto-resolve then close)" artifacts: - path: "prisma/schema.prisma" provides: "JobOrder model with status lifecycle and ticket relation" contains: "model JobOrder" - path: "src/lib/services/job-order-service.ts" provides: "Job order CRUD, status transitions, ticket-job synchronization" exports: ["JobOrderService"] - path: "src/lib/__tests__/job-order-service.test.ts" provides: "Integration tests for job order lifecycle and ticket sync" min_lines: 100 key_links: - from: "src/lib/services/job-order-service.ts" to: "src/lib/services/ticket-service.ts" via: "Auto-resolves ticket when all job orders completed" pattern: "TicketService|resolveTicket" - from: "src/app/api/tickets/[id]/job-orders/route.ts" to: "src/lib/services/job-order-service.ts" via: "POST creates job order from ticket" pattern: "createJobOrder" --- Build the job order workflow that converts tickets into assignable technician work. Purpose: Job orders are how tickets become actionable work for technicians. A ticket can spawn multiple job orders (different visits or skill types). The critical synchronization rule: when all job orders on a ticket complete, the ticket auto-resolves, and staff then manually closes after confirming with the subscriber. Output: JobOrder Prisma model, job order CRUD with status lifecycle, ticket-to-job conversion, auto-resolution sync, technician self-service status updates, integration 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/03-operational-modules/03-CONTEXT.md @.planning/phases/03-operational-modules/03-03-SUMMARY.md @prisma/schema.prisma @src/lib/services/ticket-service.ts Task 1: JobOrder Prisma model + migration prisma/schema.prisma, src/lib/prisma-tenant.ts **New enums:** - `JobOrderStatus { PENDING, IN_PROGRESS, COMPLETED, CANCELLED }` - `JobType { INSTALLATION, REPAIR, MAINTENANCE, RELOCATION, DISCONNECTION, OTHER }` **JobOrder model:** - id (uuid PK), tenantId - orderNumber (String) — auto-generated sequential per tenant, e.g., "JO-0001" - ticketId (FK to Ticket) — parent ticket - assignedToId (FK to User) — the technician - jobType (JobType) - description (String) — what needs to be done - status (JobOrderStatus default PENDING) - scheduledDate (DateTime?) — optional scheduled date - startedAt (DateTime?) — when technician started work - completedAt (DateTime?) — when work was completed - outcomeNotes (String?) — technician fills in on completion - cancelledAt (DateTime?) - cancelReason (String?) - createdById (FK to User) — staff who created the job order - createdAt, updatedAt - @@unique([tenantId, orderNumber]) - @@index([tenantId]), @@index([tenantId, assignedToId]), @@index([tenantId, status]), @@index([ticketId]) **Update relations:** - Ticket: add `jobOrders JobOrder[]` - User: add `assignedJobOrders JobOrder[] @relation("JobOrderAssignedTo")`, `createdJobOrders JobOrder[] @relation("JobOrderCreatedBy")` **Add to TENANT_SCOPED_MODELS:** "jobOrder" Run `npx prisma migrate dev --name add-job-orders` - `npx prisma migrate dev` completes without errors - `npx prisma generate` succeeds - Schema has JobOrder model with correct enums and relations JobOrder model exists with status lifecycle, ticket relation (1:many), technician assignment, and job type classification. Migration applied. Task 2: JobOrderService + API routes + integration tests src/lib/services/job-order-service.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/app/api/tickets/[id]/job-orders/route.ts, src/lib/__tests__/job-order-service.test.ts **JobOrderService** (`src/lib/services/job-order-service.ts`): - `createJobOrder(db, { ticketId, assignedToId, jobType, description, scheduledDate?, createdById })`: 1. Validate ticket exists and is not CLOSED 2. Validate assignedToId is a user with TECHNICIAN role 3. Auto-generate orderNumber (JO-NNNN per tenant) 4. Create job order with status PENDING 5. If ticket status is OPEN, auto-transition ticket to ASSIGNED (via TicketService.assignTicket with the first technician) 6. Return job order with ticket and technician info - `updateJobOrder(db, jobOrderId, { description?, scheduledDate?, jobType? })` — update editable fields - `updateStatus(db, jobOrderId, { status, outcomeNotes?, cancelReason? })`: Status transitions: - PENDING -> IN_PROGRESS: set startedAt - PENDING -> CANCELLED: set cancelledAt, cancelReason - IN_PROGRESS -> COMPLETED: set completedAt, outcomeNotes (required). Then call `checkTicketAutoResolve`. - IN_PROGRESS -> CANCELLED: set cancelledAt, cancelReason - All other transitions: throw error - `checkTicketAutoResolve(db, ticketId)`: 1. Load all job orders for this ticket 2. If ALL non-cancelled job orders have status COMPLETED, auto-resolve the ticket via TicketService.resolveTicket 3. If there are only cancelled job orders (no completed ones), do NOT auto-resolve - `reassignJobOrder(db, jobOrderId, newAssignedToId)` — reassign to different technician (only if PENDING or IN_PROGRESS) - `getJobOrder(db, jobOrderId)` — get detail with ticket, subscriber, technician info - `listJobOrders(db, filters)` — list with filters: assignedToId, status, jobType, ticketId, dateFrom, dateTo. Pagination. Sort by createdAt DESC. - `getTechnicianJobOrders(db, technicianId, filters)` — convenience wrapper for technician self-service view **API Routes:** - `POST /api/tickets/[id]/job-orders` — create job order from ticket. ADMIN, OFFICE_STAFF. Body: { assignedToId, jobType, description, scheduledDate? } - `GET /api/job-orders` — list job orders with filters. ADMIN, OFFICE_STAFF see all. TECHNICIAN sees assigned only. - `GET /api/job-orders/[id]` — get job order detail - `PUT /api/job-orders/[id]` — update job order fields. ADMIN, OFFICE_STAFF. - `POST /api/job-orders/[id]/status` — update status. TECHNICIAN can update own (PENDING->IN_PROGRESS, IN_PROGRESS->COMPLETED). ADMIN, OFFICE_STAFF can do any valid transition. Body: { status, outcomeNotes?, cancelReason? } **Integration Tests** (`src/lib/__tests__/job-order-service.test.ts`): - Create job order from ticket (auto-assigns ticket to ASSIGNED status) - One ticket can have multiple job orders - Status transitions: PENDING -> IN_PROGRESS -> COMPLETED (happy path) - Completing last job order auto-resolves parent ticket - Completing one of two job orders does NOT resolve ticket - All job orders completed -> ticket auto-resolved -> staff closes ticket - Cancelled job orders are excluded from auto-resolve check - Cannot complete job order without outcomeNotes - Invalid transitions rejected (e.g., COMPLETED -> IN_PROGRESS) - Reassign job order to different technician - Technician filter returns only their assigned orders - Job order number sequential per tenant (JO-0001, JO-0002...) - Cross-tenant isolation - `npx vitest run src/lib/__tests__/job-order-service.test.ts` — all tests pass - `npx vitest run` — full suite passes (no regressions) JobOrderService handles job order lifecycle with ticket auto-resolution sync. Technicians update their own work, staff manages assignments. All status transitions enforced. Integration tests prove the ticket-to-job-order-to-resolution flow. - Create job order from ticket: ticket auto-transitions to ASSIGNED - One ticket, multiple job orders: each tracks independently - Status lifecycle: PENDING -> IN_PROGRESS -> COMPLETED with proper guards - Completion requires outcomeNotes - Auto-resolution: all non-cancelled job orders COMPLETED -> ticket RESOLVED - Staff closes ticket (RESOLVED -> CLOSED) as separate manual step - Technician sees only their assigned job orders - Job order numbers sequential per tenant - All existing tests pass (no regressions) - JobOrder model with status lifecycle, ticket 1:many relation, and technician assignment - JobOrderService handles creation from ticket, status transitions, auto-resolution sync - Technicians can update their own job orders (view assigned, update status) - Ticket auto-resolves when all non-cancelled job orders complete - API routes enforce RBAC (staff creates, technician updates own) - Integration tests prove full ticket-to-job-to-resolution workflow After completion, create `.planning/phases/03-operational-modules/03-04-SUMMARY.md`