--- 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/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 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)" - "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" artifacts: - path: "prisma/schema.prisma" provides: "JobOrder model with status enum and ticket relation" contains: "model JobOrder" - path: "src/lib/services/job-order-service.ts" provides: "Job order CRUD, status transitions, ticket synchronization" exports: ["createJobOrder", "updateJobOrderStatus", "getJobOrder", "listJobOrders"] - path: "src/lib/services/ticket-service.ts" provides: "Updated with resolveTicket and revertToOpen calls from job order service" contains: "resolveTicket" - path: "src/lib/__tests__/job-order-service.test.ts" provides: "Integration tests for job order lifecycle and ticket sync" min_lines: 150 key_links: - from: "src/lib/services/job-order-service.ts" to: "src/lib/services/ticket-service.ts" via: "checkTicketAutoResolve and checkTicketRevertToOpen after status changes" pattern: "resolveTicket|transitionTicketStatus" - from: "src/app/api/job-orders/[id]/status/route.ts" to: "src/lib/services/job-order-service.ts" via: "updateJobOrderStatus with technician self-service" pattern: "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. @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-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 }` 2. 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]) 3. Add reverse relations: - Ticket: `jobOrders JobOrder[]` - User: `assignedJobOrders JobOrder[] @relation("JobOrderAssignedTo")`, `createdJobOrders JobOrder[] @relation("JobOrderCreatedBy")` 4. Run `npx prisma migrate dev --name add-job-orders` 5. 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. 2. 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 3. 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? }) 4. 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 - 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 After completion, create `.planning/phases/03-operational-modules/03-04-SUMMARY.md`