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:
73
prisma/migrations/20260304233528_add_tickets/migration.sql
Normal file
73
prisma/migrations/20260304233528_add_tickets/migration.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TicketStatus" AS ENUM ('OPEN', 'ASSIGNED', 'RESOLVED', 'CLOSED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TicketPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TicketSource" AS ENUM ('STAFF', 'SUBSCRIBER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TicketCategory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TicketCategory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Ticket" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"ticketNumber" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"categoryId" TEXT NOT NULL,
|
||||
"priority" "TicketPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"status" "TicketStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"source" "TicketSource" NOT NULL DEFAULT 'STAFF',
|
||||
"subscriberId" TEXT,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"closedAt" TIMESTAMP(3),
|
||||
"notes" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Ticket_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TicketCategory_tenantId_idx" ON "TicketCategory"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TicketCategory_tenantId_name_key" ON "TicketCategory"("tenantId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Ticket_tenantId_idx" ON "Ticket"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Ticket_tenantId_status_idx" ON "Ticket"("tenantId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Ticket_tenantId_categoryId_idx" ON "Ticket"("tenantId", "categoryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Ticket_subscriberId_idx" ON "Ticket"("subscriberId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Ticket_tenantId_ticketNumber_key" ON "Ticket"("tenantId", "ticketNumber");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "TicketCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "Subscriber"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user