Files
NetForge/.planning/phases/03-operational-modules/03-04-PLAN.md
kevin-asprec ef0150654b 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) — sequential
- Ready for execution

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

9.7 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/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
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)
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)
path provides contains
prisma/schema.prisma JobOrder model with status lifecycle and ticket relation model JobOrder
path provides exports
src/lib/services/job-order-service.ts Job order CRUD, status transitions, ticket-job synchronization
JobOrderService
path provides min_lines
src/lib/__tests__/job-order-service.test.ts Integration tests for job order lifecycle and ticket sync 100
from to via pattern
src/lib/services/job-order-service.ts src/lib/services/ticket-service.ts Auto-resolves ticket when all job orders completed TicketService|resolveTicket
from to via pattern
src/app/api/tickets/[id]/job-orders/route.ts src/lib/services/job-order-service.ts POST creates job order from ticket 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.

<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-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)

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/03-operational-modules/03-04-SUMMARY.md`