Files
NetForge/.planning/phases/03-operational-modules/03-03-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

221 lines
11 KiB
Markdown

---
phase: 03-operational-modules
plan: "03"
type: execute
wave: 1
depends_on: []
files_modified:
- prisma/schema.prisma
- src/lib/prisma-tenant.ts
- src/lib/services/ticket-service.ts
- src/lib/services/ticket-category-service.ts
- src/app/api/tickets/route.ts
- src/app/api/tickets/[id]/route.ts
- src/app/api/tickets/[id]/status/route.ts
- src/app/api/ticket-categories/route.ts
- src/app/api/ticket-categories/[id]/route.ts
- src/lib/__tests__/ticket-service.test.ts
autonomous: true
must_haves:
truths:
- "Staff can create a support ticket from a client call with issue description, priority, and category"
- "Tickets follow a lifecycle: OPEN -> ASSIGNED -> RESOLVED -> CLOSED"
- "Admin can configure ticket categories per tenant (create, edit, deactivate)"
- "Default ticket categories are seeded at tenant creation"
- "Ticket model supports both staff and subscriber as source types from the start"
artifacts:
- path: "prisma/schema.prisma"
provides: "Ticket and TicketCategory models"
contains: "model Ticket"
- path: "src/lib/services/ticket-service.ts"
provides: "Ticket CRUD, status transitions, search/filter"
exports: ["TicketService"]
- path: "src/lib/services/ticket-category-service.ts"
provides: "TicketCategory CRUD with default seeding"
exports: ["TicketCategoryService"]
- path: "src/lib/__tests__/ticket-service.test.ts"
provides: "Integration tests for ticket lifecycle and category management"
min_lines: 100
key_links:
- from: "src/lib/services/ticket-service.ts"
to: "prisma/schema.prisma"
via: "Prisma queries on Ticket model"
pattern: "prisma\\.ticket\\."
- from: "src/lib/services/ticket-category-service.ts"
to: "src/lib/tenant.ts"
via: "Categories seeded during tenant creation"
pattern: "seedTicketCategories|createTenant"
---
<objective>
Build the ticketing system for tracking customer support issues.
Purpose: Tickets are how customer issues enter the system — staff creates a ticket from a client call, the ticket flows through a lifecycle, and in 03-04 tickets get converted to job orders. The model also supports subscriber-created tickets (Phase 5 portal) from the start to avoid rework.
Output: Ticket and TicketCategory Prisma models, ticket CRUD service with lifecycle management, admin-configurable categories with default seeds, 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
@prisma/schema.prisma
@src/lib/tenant.ts
@src/lib/services/subscriber-service.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Ticket and TicketCategory Prisma models + category seeding</name>
<files>prisma/schema.prisma, src/lib/prisma-tenant.ts, src/lib/tenant.ts, src/lib/services/ticket-category-service.ts</files>
<action>
**New enums:**
- `TicketStatus { OPEN, ASSIGNED, RESOLVED, CLOSED }`
- `TicketPriority { LOW, MEDIUM, HIGH, URGENT }`
- `TicketSource { STAFF, SUBSCRIBER }` — supports both sources from day one
**TicketCategory model:**
- id (uuid PK), tenantId
- name (String) — e.g., "No Connection", "Slow Speed"
- description (String?)
- isActive (Boolean default true) — soft delete for deactivation
- createdAt, updatedAt
- @@unique([tenantId, name])
- @@index([tenantId])
- Relation: tickets Ticket[]
**Ticket model:**
- id (uuid PK), tenantId
- ticketNumber (String) — auto-generated sequential per tenant, e.g., "TKT-0001"
- subscriberId (FK to Subscriber) — the affected subscriber
- categoryId (FK to TicketCategory)
- source (TicketSource default STAFF)
- createdById (FK to User) — staff who created, or subscriber user in Phase 5
- assignedToId (String? FK to User) — assigned staff member (set when status -> ASSIGNED)
- subject (String) — brief issue summary
- description (String) — detailed issue description
- priority (TicketPriority default MEDIUM)
- status (TicketStatus default OPEN)
- resolvedAt (DateTime?) — when auto-resolved (all job orders completed)
- closedAt (DateTime?) — when staff manually closes after confirming resolution
- closedById (String? FK to User)
- notes (String?) — internal notes
- createdAt, updatedAt
- @@unique([tenantId, ticketNumber])
- @@index([tenantId]), @@index([tenantId, status]), @@index([tenantId, subscriberId]), @@index([tenantId, categoryId])
**Update relations:**
- Subscriber: add `tickets Ticket[]`
- User: add appropriate ticket relations (createdTickets, assignedTickets, closedTickets)
**Add to TENANT_SCOPED_MODELS:** "ticket", "ticketCategory"
**TicketCategoryService** (`src/lib/services/ticket-category-service.ts`):
- `seedDefaultCategories(db, tenantId)` — creates default categories: No Connection, Slow Speed, Billing Inquiry, New Installation, Equipment Issue, Other
- `createCategory(db, { name, description })` — admin creates custom category
- `updateCategory(db, categoryId, { name?, description?, isActive? })` — admin edits/deactivates
- `listCategories(db, { includeInactive? })` — list categories
**Update tenant creation** in `src/lib/tenant.ts`:
- After seedChartOfAccounts in the createTenant $transaction, call seedDefaultCategories to provision default ticket categories for new tenants.
Run `npx prisma migrate dev --name add-tickets`
</action>
<verify>
- `npx prisma migrate dev` completes without errors
- `npx prisma generate` succeeds
- Schema has Ticket and TicketCategory models
- Creating a new tenant seeds 6 default ticket categories
</verify>
<done>Ticket and TicketCategory models exist, default categories seeded at tenant creation, TENANT_SCOPED_MODELS updated, migration applied.</done>
</task>
<task type="auto">
<name>Task 2: TicketService + API routes + integration tests</name>
<files>src/lib/services/ticket-service.ts, src/app/api/tickets/route.ts, src/app/api/tickets/[id]/route.ts, src/app/api/tickets/[id]/status/route.ts, src/app/api/ticket-categories/route.ts, src/app/api/ticket-categories/[id]/route.ts, src/lib/__tests__/ticket-service.test.ts</files>
<action>
**TicketService** (`src/lib/services/ticket-service.ts`):
- `createTicket(db, { subscriberId, categoryId, subject, description, priority, source, createdById })`:
1. Auto-generate ticketNumber (pattern: TKT-NNNN, sequential per tenant — same approach as INV/JE numbers)
2. Create ticket with status OPEN
3. Return ticket with subscriber and category info
- `updateTicket(db, ticketId, { subject?, description?, priority?, categoryId?, notes? })` — update editable fields (not status — status has dedicated transitions)
- `assignTicket(db, ticketId, assignedToId)` — set assignedToId, transition status OPEN -> ASSIGNED
- `resolveTicket(db, ticketId)` — transition to RESOLVED (called by job order completion sync in 03-04, or manually). Set resolvedAt.
- `closeTicket(db, ticketId, closedById)` — transition RESOLVED -> CLOSED. Set closedAt, closedById. This is the manual confirmation step.
- `reopenTicket(db, ticketId)` — RESOLVED -> OPEN (if issue not actually fixed). Clear resolvedAt.
- `getTicket(db, ticketId)` — get ticket with subscriber, category, creator, assignee, and job orders (empty array until 03-04)
- `listTickets(db, filters)` — list with filters: status, priority, categoryId, subscriberId, assignedToId, dateFrom, dateTo. Pagination (skip/take). Sort by createdAt DESC.
**Status transition rules (enforce in service):**
- OPEN -> ASSIGNED (requires assignedToId)
- OPEN -> CLOSED (cancel without resolving)
- ASSIGNED -> OPEN (unassign)
- ASSIGNED -> RESOLVED (direct resolve without job order)
- RESOLVED -> CLOSED (staff confirmation)
- RESOLVED -> OPEN (reopen)
- All other transitions: throw error
**API Routes:**
- `GET /api/tickets` — list tickets with filters. ADMIN, OFFICE_STAFF see all. TECHNICIAN sees assigned only.
- `POST /api/tickets` — create ticket. ADMIN, OFFICE_STAFF. Body: { subscriberId, categoryId, subject, description, priority? }
- `GET /api/tickets/[id]` — get ticket detail
- `PUT /api/tickets/[id]` — update ticket fields. ADMIN, OFFICE_STAFF.
- `POST /api/tickets/[id]/status` — change ticket status. Body: { status, assignedToId? }. ADMIN, OFFICE_STAFF.
- `GET /api/ticket-categories` — list categories. All authenticated users.
- `POST /api/ticket-categories` — create category. ADMIN only.
- `PUT /api/ticket-categories/[id]` — update/deactivate category. ADMIN only.
**Integration Tests** (`src/lib/__tests__/ticket-service.test.ts`):
- Create ticket with auto-generated ticket number
- Ticket number sequential within tenant (TKT-0001, TKT-0002...)
- Status transitions: OPEN -> ASSIGNED -> RESOLVED -> CLOSED (happy path)
- Invalid transition rejected (e.g., OPEN -> RESOLVED without assignment — actually allowed per rules above, test the invalid ones: CLOSED -> OPEN)
- Reopen ticket (RESOLVED -> OPEN)
- List tickets with filters (status, priority, category)
- Category CRUD (create, update, deactivate)
- Deactivated category cannot be used for new tickets
- Default categories seeded on tenant creation
- Cross-tenant isolation
</action>
<verify>
- `npx vitest run src/lib/__tests__/ticket-service.test.ts` — all tests pass
- `npx vitest run` — full suite passes (no regressions)
</verify>
<done>TicketService handles ticket lifecycle (OPEN -> ASSIGNED -> RESOLVED -> CLOSED) with proper status transition enforcement. Ticket categories are admin-configurable with ISP defaults seeded at tenant creation. API routes enforce RBAC. Integration tests prove lifecycle and tenant isolation.</done>
</task>
</tasks>
<verification>
- Ticket CRUD: create, update, get, list with filters
- Status lifecycle: OPEN -> ASSIGNED -> RESOLVED -> CLOSED with proper guards
- Reopen: RESOLVED -> OPEN works
- Category management: CRUD with deactivation
- Default categories seeded at tenant creation (6 ISP-relevant categories)
- Ticket numbers are sequential per tenant
- Source field supports STAFF and SUBSCRIBER (Phase 5 ready)
- All existing tests pass (no regressions)
</verification>
<success_criteria>
- Ticket and TicketCategory models with migration applied
- TicketService handles full ticket lifecycle with enforced state transitions
- Admin-configurable categories with 6 defaults seeded at tenant creation
- Ticket model supports both staff and subscriber source types
- API routes enforce RBAC (staff creates, technician views assigned)
- Integration tests prove lifecycle, filtering, and tenant isolation
</success_criteria>
<output>
After completion, create `.planning/phases/03-operational-modules/03-03-SUMMARY.md`
</output>