feat(03-03): Ticket schema, categories, migration, and tenant scoping

- Added TicketStatus, TicketPriority, TicketSource enums to schema
- Added TicketCategory model (tenant-scoped, unique per tenant by name)
- Added Ticket model with lifecycle fields (status, priority, source, resolvedAt, closedAt)
- Added reverse relations: Subscriber.tickets, User.createdTickets, TicketCategory.tickets
- Applied migration 20260304233528_add_tickets
- Added ticket and ticketCategory to TENANT_SCOPED_MODELS with full extension blocks
- Seeded 6 default ISP ticket categories in createTenant transaction
This commit is contained in:
kevin-asprec
2026-03-05 07:36:45 +08:00
parent 2321bf2ada
commit b0562a0a12
4 changed files with 357 additions and 1 deletions

View File

@@ -100,6 +100,25 @@ enum PaymentStatus {
VOIDED
}
enum TicketStatus {
OPEN
ASSIGNED
RESOLVED
CLOSED
}
enum TicketPriority {
LOW
MEDIUM
HIGH
URGENT
}
enum TicketSource {
STAFF
SUBSCRIBER
}
// =============================================================================
// MODELS
// =============================================================================
@@ -258,6 +277,7 @@ model Subscriber {
invoices Invoice[]
payments Payment[]
tickets Ticket[]
/// Account numbers must be unique within a tenant
@@unique([tenantId, accountNumber])
@@ -340,6 +360,8 @@ model User {
recordedPayments Payment[] @relation("PaymentRecordedBy")
/// Zone assignments for collector role (which zones this user can collect from)
zoneAssignments ZoneAssignment[]
/// Tickets this user created (staff or subscriber portal)
createdTickets Ticket[] @relation("TicketCreatedBy")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -529,6 +551,63 @@ model PaymentAllocation {
@@index([tenantId])
}
/// A TicketCategory is an admin-configurable label for classifying support tickets.
/// Default ISP categories are seeded at tenant creation.
/// Deactivated categories (isActive=false) cannot be used for new tickets.
model TicketCategory {
id String @id @default(uuid())
tenantId String
name String
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tickets Ticket[]
/// Category names must be unique within a tenant
@@unique([tenantId, name])
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
}
/// A Ticket records a support request from a subscriber or staff member.
/// Tickets follow the lifecycle: OPEN -> ASSIGNED -> RESOLVED -> CLOSED.
/// Status transitions are validated by the ticket service guard map.
model Ticket {
id String @id @default(uuid())
tenantId String
/// Auto-generated sequential identifier per tenant (e.g., "TKT-0001")
ticketNumber String
subject String
description String
categoryId String
category TicketCategory @relation(fields: [categoryId], references: [id])
priority TicketPriority @default(MEDIUM)
status TicketStatus @default(OPEN)
source TicketSource @default(STAFF)
/// The subscriber this ticket is about (optional — not all tickets are subscriber-specific)
subscriberId String?
subscriber Subscriber? @relation(fields: [subscriberId], references: [id])
/// The staff member or subscriber who created this ticket
createdById String
createdBy User @relation("TicketCreatedBy", fields: [createdById], references: [id])
resolvedAt DateTime?
closedAt DateTime?
/// Internal staff notes
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// Ticket numbers must be unique within a tenant
@@unique([tenantId, ticketNumber])
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
@@index([tenantId, status])
@@index([tenantId, categoryId])
@@index([subscriberId])
}
/// A single line item on an Invoice (e.g., "50 Mbps Monthly Service — $49.99").
model InvoiceLine {
id String @id @default(uuid())