diff --git a/.planning/STATE.md b/.planning/STATE.md index 3238f9b..8c7e782 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -10,11 +10,11 @@ See: .planning/PROJECT.md (updated 2026-03-04) ## Current Position Phase: 3 of 5 (Operational Modules) — In progress -Plan: 1 of 5 in phase 3 (12/20 total complete) -Status: Phase 3 started. 03-01 (Zone Management) complete. Zones, ZoneAssignment, collector scoping all implemented. -Last activity: 2026-03-05 — Completed 03-01-PLAN.md (Zone management, 25 tests) +Plan: 3 of 5 in phase 3 (14/20 total complete) +Status: Phase 3 in progress. 03-01 (Zones), 03-02 (skipped/not present), 03-03 (Ticketing) complete. +Last activity: 2026-03-05 — Completed 03-03-PLAN.md (Ticketing system, 28 tests) -Progress: [████████████░] 60% (12/20 plans across all phases) +Progress: [█████████████░] 70% (14/20 plans across all phases) ## Performance Metrics @@ -100,6 +100,11 @@ Recent decisions affecting current work: - [03-01]: Subscriber.zone String? replaced with Subscriber.zoneId FK — required for relational queries and JOIN-based ordering - [03-01]: COLLECTOR gets can("read", "Zone") in CASL: coarse-grained gate, data layer enforces which specific zones - [03-01]: Migration applied via Docker exec psql + prisma migrate resolve --applied (non-interactive CLI workaround) +- [03-03]: VALID_TICKET_TRANSITIONS guard map: OPEN->[ASSIGNED,CLOSED], ASSIGNED->[OPEN,RESOLVED], RESOLVED->[CLOSED,OPEN], CLOSED->[] (terminal) +- [03-03]: resolveTicket is idempotent — checks if already RESOLVED and returns silently, preventing race conditions from multiple job completions +- [03-03]: Ticket cleanup order in tests: tickets -> ticketCategories -> subscribers -> ... (categories seeded by createTenant must be deleted on teardown) +- [03-03]: transitionTicketStatus is the single gateway for status changes — updateTicket explicitly excludes status field +- [03-03]: 6 default ISP categories seeded in createTenant $transaction (No Connection, Slow Speed, Billing Inquiry, New Installation, Equipment Issue, Other) ### Pending Todos @@ -117,6 +122,6 @@ None. ## Session Continuity -Last session: 2026-03-05T07:31:00Z -Stopped at: Completed 03-01-PLAN.md (Zone management — 25 tests, Zone/ZoneAssignment models, collector scoping) +Last session: 2026-03-04T23:40:15Z +Stopped at: Completed 03-03-PLAN.md (Ticketing system — 28 tests, Ticket/TicketCategory models, guard-map lifecycle, 7 API routes) Resume file: None diff --git a/.planning/phases/03-operational-modules/03-03-SUMMARY.md b/.planning/phases/03-operational-modules/03-03-SUMMARY.md new file mode 100644 index 0000000..b1168ed --- /dev/null +++ b/.planning/phases/03-operational-modules/03-03-SUMMARY.md @@ -0,0 +1,147 @@ +--- +phase: 03-operational-modules +plan: "03" +subsystem: api +tags: [prisma, postgresql, tickets, support, lifecycle, tenant-scoping, vitest] + +# Dependency graph +requires: + - phase: 03-01 + provides: Zone and ZoneAssignment models already in schema (no conflicts) + - phase: 02-03 + provides: Subscriber model (tickets reference subscribers) + - phase: 01-03 + provides: withTenantContext() extension pattern for tenant scoping + - phase: 01-04 + provides: withPermission() HOF for API route auth + +provides: + - 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 + +affects: + - 03-04 (job orders link to tickets, trigger OPEN->ASSIGNED and auto-resolve) + - 05 (subscriber portal uses TicketSource.SUBSCRIBER) + +# Tech tracking +tech-stack: + 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" + +key-files: + created: + - 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 + modified: + - prisma/schema.prisma + - src/lib/prisma-tenant.ts + - src/lib/tenant.ts + +key-decisions: + - "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)" + +patterns-established: + - "Guard map pattern: export const VALID_TRANSITIONS: Record for validating state machine transitions" + - "Idempotent convenience wrappers: resolve/close/etc. that check current state and no-op if already in target state" + +# Metrics +duration: 6min +completed: 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) 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*