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>
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 |
|
|
true |
|
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 }`-
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])
-
Add reverse relations:
- Ticket:
jobOrders JobOrder[] - User:
assignedJobOrders JobOrder[] @relation("JobOrderAssignedTo"),createdJobOrders JobOrder[] @relation("JobOrderCreatedBy")
- Ticket:
-
Run
npx prisma migrate dev --name add-job-orders -
Add JobOrder to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension block.
npx prisma migrate devsucceedsnpx tsc --noEmitpasses- Grep prisma-tenant.ts confirms "jobOrder" in TENANT_SCOPED_MODELS JobOrder model exists with ticket relation, migration applied, tenant scoping configured.
-
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 orderupdateJobOrderStatus(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, createdBylistJobOrders(tenantPrisma, { ticketId?, assignedToId?, status?, page?, limit? })— filtered listgetMyJobOrders(tenantPrisma, technicianUserId, { status?, page?, limit? })— convenience for technician self-service
- Sequential number generation:
-
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? })
-
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 passnpx tsc --noEmitpasses 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.
<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>