Files
kevin-asprec 570218320c docs(03-03): complete Ticketing System plan
Tasks completed: 2/2
- Task 1: Ticket schema, categories, migration, and tenant scoping
- Task 2: Ticket service, category service, API routes, and 28 integration tests

SUMMARY: .planning/phases/03-operational-modules/03-03-SUMMARY.md
2026-03-05 07:41:39 +08:00

7.3 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established duration completed
03-operational-modules 03 api
prisma
postgresql
tickets
support
lifecycle
tenant-scoping
vitest
phase provides
03-01 Zone and ZoneAssignment models already in schema (no conflicts)
phase provides
02-03 Subscriber model (tickets reference subscribers)
phase provides
01-03 withTenantContext() extension pattern for tenant scoping
phase provides
01-04 withPermission() HOF for API route auth
Ticket model with status lifecycle (OPEN->ASSIGNED->RESOLVED->CLOSED)
TicketCategory model with isActive soft-deactivation
ticket-service.ts
CRUD, sequential numbering (TKT-NNNN), guard-map transitions, idempotent resolve
ticket-category-service.ts
CRUD with deactivated category validation
5 ticket API routes and 2 category API routes
Default 6 ISP categories seeded at tenant creation
03-04 (job orders link to tickets, trigger OPEN->ASSIGNED and auto-resolve)
05 (subscriber portal uses TicketSource.SUBSCRIBER)
added patterns
Guard map for status transitions: VALID_TICKET_TRANSITIONS maps each status to allowed next states
Idempotent resolveTicket: checks current status before transition, no-ops if already RESOLVED
Sequential TKT-NNNN numbering: same pattern as INV-YYYY-NNNN and JE-YYYY-NNNN
Deactivated category validation: ticket creation checks isActive=true before proceeding
Default category seeding: 6 ISP categories seeded inside createTenant $transaction
created modified
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
prisma/migrations/20260304233528_add_tickets/migration.sql
prisma/schema.prisma
src/lib/prisma-tenant.ts
src/lib/tenant.ts
VALID_TICKET_TRANSITIONS guard map: OPEN->[ASSIGNED,CLOSED], ASSIGNED->[OPEN,RESOLVED], RESOLVED->[CLOSED,OPEN], CLOSED->[] (terminal)
resolveTicket is idempotent: checks if already RESOLVED and returns silently — prevents race conditions when multiple job completions trigger auto-resolve
TicketSource.SUBSCRIBER added for Phase 5 subscriber portal compatibility — forward-compatible enum
Deactivated categories rejected at createTicket (not just at category level) — enforced at service layer
Default 6 categories seeded in createTenant $transaction: No Connection, Slow Speed, Billing Inquiry, New Installation, Equipment Issue, Other
transitionTicketStatus is the single gateway for status changes — updateTicket explicitly excludes status field
Ticket cleanup order in tests: tickets -> ticketCategories -> subscribers -> ... (categories seeded by createTenant must be deleted on teardown)
Guard map pattern: export const VALID_TRANSITIONS: Record<Status, Status[]> for validating state machine transitions
Idempotent convenience wrappers: resolve/close/etc. that check current state and no-op if already in target state
6min 2026-03-05

Phase 3 Plan 03: Ticketing System Summary

Ticket/TicketCategory Prisma models, guard-map lifecycle (OPEN->ASSIGNED->RESOLVED->CLOSED), 6 default ISP category seeds, TKT-NNNN sequential numbering, 5 API routes, and 28 passing integration tests

Performance

  • Duration: 6 min
  • Started: 2026-03-04T23:34:17Z
  • Completed: 2026-03-04T23:40:15Z
  • Tasks: 2
  • Files modified: 11 (3 modified, 8 created, 1 migration)

Accomplishments

  • Ticket and TicketCategory models with full tenant scoping (TENANT_SCOPED_MODELS extended)
  • Status lifecycle enforced by VALID_TICKET_TRANSITIONS guard map — CLOSED is terminal, invalid transitions throw
  • resolveTicket() is idempotent (silently no-ops if already RESOLVED, preventing race conditions from job order completions)
  • 6 default ISP ticket categories seeded atomically inside createTenant transaction
  • 7 API routes with withPermission() guards (manage permission for category write operations)
  • 28 integration tests covering lifecycle, deactivated category rejection, cross-tenant isolation

Task Commits

Each task was committed atomically:

  1. Task 1: Ticket schema, categories, migration, and tenant scoping - b0562a0 (feat)
  2. Task 2: Ticket service, category service, API routes, and integration tests - 74d26d9 (feat)

Files Created/Modified

  • prisma/schema.prisma - Added TicketStatus, TicketPriority, TicketSource enums; TicketCategory and Ticket models; reverse relations on Subscriber and User
  • prisma/migrations/20260304233528_add_tickets/migration.sql - Migration for tickets/ticket_categories tables
  • src/lib/prisma-tenant.ts - Added ticket and ticketCategory to TENANT_SCOPED_MODELS with full 12-operation extension blocks
  • src/lib/tenant.ts - Seeded 6 default ticket categories inside createTenant $transaction
  • src/lib/services/ticket-service.ts - createTicket, updateTicket, getTicket, listTickets, transitionTicketStatus, resolveTicket
  • src/lib/services/ticket-category-service.ts - createCategory, updateCategory, listCategories
  • src/app/api/tickets/route.ts - GET (list with filters) and POST (create) ticket endpoints
  • src/app/api/tickets/[id]/route.ts - GET and PUT ticket by ID endpoints
  • src/app/api/tickets/[id]/status/route.ts - POST status transition endpoint
  • src/app/api/ticket-categories/route.ts - GET (list) and POST (create, manage permission) category endpoints
  • src/app/api/ticket-categories/[id]/route.ts - PUT category update (manage permission) endpoint
  • src/lib/__tests__/ticket-service.test.ts - 28 integration tests

Decisions Made

  • Used VALID_TICKET_TRANSITIONS guard map (Record<TicketStatus, TicketStatus[]>) instead of switch/case — enables exhaustive enumeration and easy extension for 03-04
  • resolveTicket idempotent by checking status before transition — per RESEARCH.md pitfall 5 (race condition on job completion)
  • CLOSED is terminal (empty array in guard map) — no reopen from CLOSED, matches real ISP workflow
  • TicketSource.SUBSCRIBER included now — forward-compatible with Phase 5 subscriber portal
  • Deactivated category check in both createTicket and updateTicket (when changing categoryId) — consistent enforcement
  • createTenant seeds categories in the existing $transaction — atomic with tenant/user/COA creation

Deviations from Plan

None - plan executed exactly as written.

Issues Encountered

None - tests passed on first run. The prisma:error log lines in test output are expected (they are the error paths being tested for thrown errors).

User Setup Required

None - no external service configuration required.

Next Phase Readiness

  • 03-04 (Job Orders) can now link jobs to tickets via ticketId FK
  • 03-04 will call transitionTicketStatus to trigger OPEN->ASSIGNED on job creation
  • 03-04 will call resolveTicket (idempotent) when all jobs for a ticket are complete
  • jobOrders relation on Ticket will be added in 03-04 (schema addition, not breaking change)

Phase: 03-operational-modules Completed: 2026-03-05