--- 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" --- 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. @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 @prisma/schema.prisma @src/lib/tenant.ts @src/lib/services/subscriber-service.ts Task 1: Ticket and TicketCategory Prisma models + category seeding prisma/schema.prisma, src/lib/prisma-tenant.ts, src/lib/tenant.ts, src/lib/services/ticket-category-service.ts **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` - `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 Ticket and TicketCategory models exist, default categories seeded at tenant creation, TENANT_SCOPED_MODELS updated, migration applied. Task 2: TicketService + API routes + integration tests 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 **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 - `npx vitest run src/lib/__tests__/ticket-service.test.ts` — all tests pass - `npx vitest run` — full suite passes (no regressions) 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. - 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) - 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 After completion, create `.planning/phases/03-operational-modules/03-03-SUMMARY.md`