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) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ files_modified:
|
||||
- src/lib/prisma-tenant.ts
|
||||
- src/lib/services/ticket-service.ts
|
||||
- src/lib/services/ticket-category-service.ts
|
||||
- src/lib/tenant.ts
|
||||
- src/app/api/tickets/route.ts
|
||||
- src/app/api/tickets/[id]/route.ts
|
||||
- src/app/api/tickets/[id]/status/route.ts
|
||||
@@ -19,41 +20,49 @@ 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"
|
||||
- "Staff can create a support ticket with subject, description, priority, and category"
|
||||
- "Tickets follow lifecycle: OPEN -> ASSIGNED -> RESOLVED -> CLOSED"
|
||||
- "Invalid status transitions are rejected (e.g., CLOSED -> OPEN is invalid)"
|
||||
- "Admin can create, update, and deactivate ticket categories"
|
||||
- "Default ISP categories are seeded at tenant creation"
|
||||
- "Tickets with a deactivated category cannot be created"
|
||||
- "Ticket data is tenant-scoped"
|
||||
artifacts:
|
||||
- path: "prisma/schema.prisma"
|
||||
provides: "Ticket and TicketCategory models"
|
||||
provides: "Ticket, TicketCategory models with enums"
|
||||
contains: "model Ticket"
|
||||
- path: "src/lib/services/ticket-service.ts"
|
||||
provides: "Ticket CRUD, status transitions, search/filter"
|
||||
exports: ["TicketService"]
|
||||
provides: "Ticket CRUD and status transitions"
|
||||
exports: ["createTicket", "updateTicket", "getTicket", "listTickets", "transitionTicketStatus"]
|
||||
- path: "src/lib/services/ticket-category-service.ts"
|
||||
provides: "TicketCategory CRUD with default seeding"
|
||||
exports: ["TicketCategoryService"]
|
||||
provides: "Category CRUD"
|
||||
exports: ["createCategory", "updateCategory", "listCategories"]
|
||||
- path: "src/lib/tenant.ts"
|
||||
provides: "Default ticket category seeding in createTenant"
|
||||
contains: "ticketCategory"
|
||||
- path: "src/lib/__tests__/ticket-service.test.ts"
|
||||
provides: "Integration tests for ticket lifecycle and category management"
|
||||
min_lines: 100
|
||||
provides: "Integration tests for ticket lifecycle, categories, transitions"
|
||||
min_lines: 150
|
||||
key_links:
|
||||
- from: "src/lib/services/ticket-service.ts"
|
||||
to: "src/lib/services/ticket-category-service.ts"
|
||||
via: "validates category isActive before ticket creation"
|
||||
pattern: "isActive"
|
||||
- from: "src/lib/tenant.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"
|
||||
via: "seeds default TicketCategory records in createTenant transaction"
|
||||
pattern: "ticketCategory\\.createMany"
|
||||
- from: "src/app/api/tickets/[id]/status/route.ts"
|
||||
to: "src/lib/services/ticket-service.ts"
|
||||
via: "transitionTicketStatus with guard map"
|
||||
pattern: "transitionTicketStatus"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the ticketing system for tracking customer support issues.
|
||||
Create the ticketing system: Ticket and TicketCategory models, ticket lifecycle (OPEN -> ASSIGNED -> RESOLVED -> CLOSED), admin-configurable categories with ISP default seeds, ticket CRUD API, status transition API with guard validation.
|
||||
|
||||
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.
|
||||
Purpose: Tickets are the intake mechanism for customer issues. They must exist before job orders (03-04) can be created from them. Category configurability avoids hardcoded enums.
|
||||
Output: Ticket/TicketCategory models, ticket-service.ts, ticket-category-service.ts, updated tenant.ts, 5 API routes, integration tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -66,153 +75,172 @@ Output: Ticket and TicketCategory Prisma models, ticket CRUD service with lifecy
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-operational-modules/03-CONTEXT.md
|
||||
@.planning/phases/03-operational-modules/03-RESEARCH.md
|
||||
@prisma/schema.prisma
|
||||
@src/lib/prisma-tenant.ts
|
||||
@src/lib/tenant.ts
|
||||
@src/lib/services/subscriber-service.ts
|
||||
@src/lib/casl/types.ts
|
||||
@src/lib/casl/permissions.ts
|
||||
@src/lib/services/payment-service.ts (pattern reference)
|
||||
@src/lib/__tests__/payment.test.ts (pattern reference)
|
||||
</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>
|
||||
<name>Task 1: Ticket schema, categories, migration, and tenant scoping</name>
|
||||
<files>
|
||||
prisma/schema.prisma
|
||||
src/lib/prisma-tenant.ts
|
||||
src/lib/tenant.ts
|
||||
</files>
|
||||
<action>
|
||||
**New enums:**
|
||||
- `TicketStatus { OPEN, ASSIGNED, RESOLVED, CLOSED }`
|
||||
- `TicketPriority { LOW, MEDIUM, HIGH, URGENT }`
|
||||
- `TicketSource { STAFF, SUBSCRIBER }` — supports both sources from day one
|
||||
1. Add enums to schema.prisma:
|
||||
- `enum TicketStatus { OPEN ASSIGNED RESOLVED CLOSED }`
|
||||
- `enum TicketPriority { LOW MEDIUM HIGH URGENT }`
|
||||
- `enum TicketSource { STAFF SUBSCRIBER }` (supports Phase 5 subscriber portal)
|
||||
|
||||
**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[]
|
||||
2. Add TicketCategory model:
|
||||
- id (uuid), tenantId, name (String), description (String?), isActive (Boolean default true), createdAt, updatedAt
|
||||
- @@unique([tenantId, name]), @@index([tenantId])
|
||||
|
||||
**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])
|
||||
3. Add Ticket model:
|
||||
- id (uuid), tenantId
|
||||
- ticketNumber (String) — auto-generated TKT-NNNN
|
||||
- subject (String), description (String)
|
||||
- categoryId (String, FK to TicketCategory)
|
||||
- priority (TicketPriority, default MEDIUM)
|
||||
- status (TicketStatus, default OPEN)
|
||||
- source (TicketSource, default STAFF)
|
||||
- subscriberId (String?, FK to Subscriber — which subscriber this ticket is about)
|
||||
- createdById (String, FK to User — staff or subscriber who created it)
|
||||
- resolvedAt (DateTime?), closedAt (DateTime?)
|
||||
- notes (String?) — internal notes
|
||||
- createdAt, updatedAt
|
||||
- Relations: category -> TicketCategory, subscriber -> Subscriber, createdBy -> User
|
||||
- @@unique([tenantId, ticketNumber]), @@index([tenantId]), @@index([tenantId, status]), @@index([tenantId, categoryId]), @@index([subscriberId])
|
||||
- Add reverse relations: Subscriber.tickets Ticket[], User.createdTickets Ticket[], TicketCategory.tickets Ticket[]
|
||||
|
||||
**Update relations:**
|
||||
- Subscriber: add `tickets Ticket[]`
|
||||
- User: add appropriate ticket relations (createdTickets, assignedTickets, closedTickets)
|
||||
4. Run `npx prisma migrate dev --name add-tickets`
|
||||
|
||||
**Add to TENANT_SCOPED_MODELS:** "ticket", "ticketCategory"
|
||||
5. Add Ticket and TicketCategory to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL extension blocks (all 12 operations each — copy from subscriber block).
|
||||
|
||||
**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`
|
||||
6. Update src/lib/tenant.ts createTenant function: after seedChartOfAccounts, add seed for default ticket categories within the same transaction:
|
||||
```
|
||||
const defaultCategories = [
|
||||
{ name: "No Connection", description: "Subscriber has no internet connection", tenantId: tenant.id },
|
||||
{ name: "Slow Speed", description: "Connection speed below expected plan speed", tenantId: tenant.id },
|
||||
{ name: "Billing Inquiry", description: "Questions about bills or payments", tenantId: tenant.id },
|
||||
{ name: "New Installation", description: "Request for new service installation", tenantId: tenant.id },
|
||||
{ name: "Equipment Issue", description: "Router, ONU, or cable problems", tenantId: tenant.id },
|
||||
{ name: "Other", description: "Other issues not covered by categories above", tenantId: tenant.id },
|
||||
];
|
||||
await tx.ticketCategory.createMany({ data: defaultCategories });
|
||||
```
|
||||
IMPORTANT: This is inside the $transaction callback, so use `tx` (raw client) and include tenantId explicitly.
|
||||
</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
|
||||
- `npx prisma migrate dev` succeeds
|
||||
- `npx tsc --noEmit` passes
|
||||
- Grep prisma-tenant.ts confirms "ticket" and "ticketCategory" in TENANT_SCOPED_MODELS
|
||||
- Grep tenant.ts confirms "ticketCategory" seeding
|
||||
</verify>
|
||||
<done>Ticket and TicketCategory models exist, default categories seeded at tenant creation, TENANT_SCOPED_MODELS updated, migration applied.</done>
|
||||
<done>Ticket and TicketCategory models exist, migration applied, tenant scoping configured, default categories seeded in tenant creation.</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>
|
||||
<name>Task 2: Ticket service, category service, API routes, and integration tests</name>
|
||||
<files>
|
||||
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
|
||||
</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.
|
||||
1. Create src/lib/services/ticket-category-service.ts:
|
||||
- `createCategory(tenantPrisma, tenantId, { name, description })` — create category
|
||||
- `updateCategory(tenantPrisma, categoryId, { name?, description?, isActive? })` — update/deactivate
|
||||
- `listCategories(tenantPrisma, { activeOnly?: boolean })` — list with optional active filter
|
||||
|
||||
**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
|
||||
2. Create src/lib/services/ticket-service.ts:
|
||||
- Sequential number generation: `generateTicketNumber(tenantPrisma)` — same pattern as invoice/JE numbers. Query `ticket.findMany({ where: { ticketNumber: { startsWith: "TKT-" } }, orderBy: { ticketNumber: "desc" }, take: 1 })`, parse last 4 digits, increment, pad to 4.
|
||||
- `createTicket(tenantPrisma, tenantId, { subject, description, categoryId, priority?, subscriberId?, createdById, source? })`:
|
||||
- Validate category exists AND isActive=true — throw if deactivated
|
||||
- Generate ticketNumber
|
||||
- Create ticket with status OPEN
|
||||
- `updateTicket(tenantPrisma, ticketId, { subject?, description?, categoryId?, priority?, notes? })` — update metadata only (NOT status)
|
||||
- `getTicket(tenantPrisma, ticketId)` — include category, subscriber, createdBy, jobOrders (empty array for now, relation added in 03-04)
|
||||
- `listTickets(tenantPrisma, { status?, categoryId?, priority?, subscriberId?, page?, limit? })` — filtered list with pagination, include category and subscriber
|
||||
- `transitionTicketStatus(tenantPrisma, ticketId, newStatus)`:
|
||||
- Define VALID_TICKET_TRANSITIONS map:
|
||||
OPEN -> [ASSIGNED, CLOSED]
|
||||
ASSIGNED -> [OPEN, RESOLVED]
|
||||
RESOLVED -> [CLOSED, OPEN]
|
||||
CLOSED -> [] (terminal)
|
||||
- Load current ticket, validate transition is allowed, update status
|
||||
- Set resolvedAt on transition to RESOLVED, closedAt on transition to CLOSED
|
||||
- NOTE: OPEN -> ASSIGNED is triggered by job order creation (03-04), not manually
|
||||
- NOTE: ASSIGNED -> RESOLVED is triggered by auto-resolve (03-04) when all jobs complete
|
||||
- Both can also be called manually by staff via API
|
||||
- `resolveTicket(tenantPrisma, ticketId)` — convenience wrapper around transitionTicketStatus that is IDEMPOTENT: if ticket is already RESOLVED, return silently (prevents race condition per RESEARCH.md pitfall 5)
|
||||
|
||||
**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.
|
||||
3. Create API routes:
|
||||
- GET /api/tickets: withPermission("read", "Ticket") -> listTickets with query param filters
|
||||
- POST /api/tickets: withPermission("create", "Ticket") -> createTicket (body: { subject, description, categoryId, priority?, subscriberId? })
|
||||
- GET /api/tickets/[id]: withPermission("read", "Ticket") -> getTicket (dynamic route pattern)
|
||||
- PUT /api/tickets/[id]: withPermission("update", "Ticket") -> updateTicket (dynamic route pattern)
|
||||
- POST /api/tickets/[id]/status: withPermission("update", "Ticket") -> transitionTicketStatus (body: { status })
|
||||
- GET /api/ticket-categories: withPermission("read", "Ticket") -> listCategories
|
||||
- POST /api/ticket-categories: withPermission("manage", "Ticket") -> createCategory (admin/staff only via manage check)
|
||||
- PUT /api/ticket-categories/[id]: withPermission("manage", "Ticket") -> updateCategory
|
||||
|
||||
**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
|
||||
4. Create src/lib/__tests__/ticket-service.test.ts:
|
||||
- Setup: create tenant (this now auto-seeds categories via updated tenant.ts), admin user
|
||||
- Test: default categories are seeded on tenant creation (6 categories)
|
||||
- Test: createCategory adds a new category
|
||||
- Test: updateCategory deactivates a category (isActive=false)
|
||||
- Test: createTicket with valid category succeeds, returns TKT-0001
|
||||
- Test: createTicket with deactivated category throws
|
||||
- Test: second ticket gets TKT-0002
|
||||
- Test: transitionTicketStatus OPEN -> CLOSED succeeds
|
||||
- Test: transitionTicketStatus CLOSED -> OPEN throws (terminal state)
|
||||
- Test: transitionTicketStatus OPEN -> RESOLVED throws (invalid)
|
||||
- Test: resolveTicket is idempotent (calling on RESOLVED ticket does not throw)
|
||||
- Test: listTickets with status filter returns correct subset
|
||||
- Test: cross-tenant isolation (Tenant B cannot see Tenant A tickets)
|
||||
- Cleanup: tickets -> ticketCategories -> subscribers -> users -> tenant
|
||||
NOTE: Since tenant creation now seeds ticketCategories, cleanup must delete them. Use tenantId filter.
|
||||
</action>
|
||||
<verify>
|
||||
- `npx vitest run src/lib/__tests__/ticket-service.test.ts` — all tests pass
|
||||
- `npx vitest run` — full suite passes (no regressions)
|
||||
- `npx tsc --noEmit` passes
|
||||
</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>
|
||||
<done>Ticket CRUD works with sequential numbering, status transitions enforce guard map, categories are admin-configurable with ISP defaults, deactivated categories rejected, idempotent resolve, cross-tenant isolation verified.</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)
|
||||
- `npx prisma migrate dev` succeeds
|
||||
- `npx tsc --noEmit` passes
|
||||
- `npx vitest run src/lib/__tests__/ticket-service.test.ts` — all tests green
|
||||
- Ticket lifecycle (OPEN -> ASSIGNED -> RESOLVED -> CLOSED) enforced
|
||||
- Default categories seeded on tenant creation
|
||||
</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
|
||||
- Ticket and TicketCategory models with tenant scoping
|
||||
- Status transition guard map rejects invalid transitions
|
||||
- Default 6 ISP categories seeded at tenant creation
|
||||
- Deactivated categories cannot be used for new tickets
|
||||
- Sequential ticket numbering (TKT-NNNN)
|
||||
- resolveTicket is idempotent
|
||||
- Cross-tenant isolation verified by test
|
||||
- All integration tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user