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>
232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<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>
|
|
|
|
<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)
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: JobOrder schema, migration, and tenant scoping</name>
|
|
<files>
|
|
prisma/schema.prisma
|
|
src/lib/prisma-tenant.ts
|
|
</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
- `npx prisma migrate dev` succeeds
|
|
- `npx tsc --noEmit` passes
|
|
- Grep prisma-tenant.ts confirms "jobOrder" in TENANT_SCOPED_MODELS
|
|
</verify>
|
|
<done>JobOrder model exists with ticket relation, migration applied, tenant scoping configured.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Job order service, ticket sync, API routes, and integration tests</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
- `npx vitest run src/lib/__tests__/job-order-service.test.ts` — all tests pass
|
|
- `npx tsc --noEmit` passes
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-operational-modules/03-04-SUMMARY.md`
|
|
</output>
|