feat(service): P4.1 support tables (queue/team_member/ticket/ticket_thread_link/state_history/callback) + events
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ export const IIOS_EVENTS = {
|
||||
messagePersisted: 'com.insignia.iios.message.persisted.v1',
|
||||
inboxItemCreated: 'com.insignia.iios.inbox.item.created.v1',
|
||||
ticketCreated: 'com.insignia.iios.support.ticket.created.v1',
|
||||
ticketStateChanged: 'com.insignia.iios.support.ticket.state.changed.v1',
|
||||
callbackRequested: 'com.insignia.iios.support.callback.requested.v1',
|
||||
notificationQueued: 'com.insignia.iios.notification.queued.v1',
|
||||
deliveryAttempted: 'com.insignia.iios.delivery.attempted.v1',
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosTicketState" AS ENUM ('NEW', 'OPEN', 'PENDING_CUSTOMER', 'PENDING_INTERNAL', 'RESOLVED', 'CLOSED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosTicketPriority" AS ENUM ('P0', 'P1', 'P2', 'P3', 'P4');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosCallbackStatus" AS ENUM ('PENDING', 'SCHEDULED', 'COMPLETED', 'MISSED', 'CANCELLED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosSupportQueue" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"supportTier" TEXT NOT NULL DEFAULT 'REGULAR',
|
||||
"assignmentPolicy" JSONB,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosSupportQueue_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosSupportTeamMember" (
|
||||
"queueId" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'AGENT',
|
||||
"maxActive" INTEGER NOT NULL DEFAULT 3,
|
||||
"activeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"availabilityState" TEXT NOT NULL DEFAULT 'OFFLINE',
|
||||
|
||||
CONSTRAINT "IiosSupportTeamMember_pkey" PRIMARY KEY ("queueId","actorId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosTicket" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"queueId" TEXT,
|
||||
"requesterActorId" TEXT,
|
||||
"assignedActorId" TEXT,
|
||||
"subject" TEXT NOT NULL,
|
||||
"state" "IiosTicketState" NOT NULL DEFAULT 'NEW',
|
||||
"priority" "IiosTicketPriority" NOT NULL DEFAULT 'P3',
|
||||
"issueClass" TEXT,
|
||||
"createdFromInteractionId" TEXT,
|
||||
"traceId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "IiosTicket_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosTicketThreadLink" (
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"threadId" TEXT NOT NULL,
|
||||
"relationKind" TEXT NOT NULL DEFAULT 'PRIMARY',
|
||||
"linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosTicketThreadLink_pkey" PRIMARY KEY ("ticketId","threadId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosTicketStateHistory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"fromState" "IiosTicketState",
|
||||
"toState" "IiosTicketState" NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"reasonCode" TEXT,
|
||||
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosTicketStateHistory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosCallbackRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"ticketId" TEXT,
|
||||
"requesterActorId" TEXT NOT NULL,
|
||||
"preferChannel" TEXT NOT NULL,
|
||||
"preferredTime" TIMESTAMP(3),
|
||||
"notes" TEXT,
|
||||
"status" "IiosCallbackStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"meetingRef" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosCallbackRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosSupportQueue_scopeId_idx" ON "IiosSupportQueue"("scopeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosSupportTeamMember_actorId_idx" ON "IiosSupportTeamMember"("actorId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosTicket_scopeId_state_idx" ON "IiosTicket"("scopeId", "state");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosTicket_assignedActorId_state_idx" ON "IiosTicket"("assignedActorId", "state");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosTicketThreadLink_threadId_idx" ON "IiosTicketThreadLink"("threadId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosTicketStateHistory_ticketId_idx" ON "IiosTicketStateHistory"("ticketId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosCallbackRequest_scopeId_status_idx" ON "IiosCallbackRequest"("scopeId", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosSupportQueue" ADD CONSTRAINT "IiosSupportQueue_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosSupportTeamMember" ADD CONSTRAINT "IiosSupportTeamMember_queueId_fkey" FOREIGN KEY ("queueId") REFERENCES "IiosSupportQueue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosSupportTeamMember" ADD CONSTRAINT "IiosSupportTeamMember_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "IiosActorRef"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicket" ADD CONSTRAINT "IiosTicket_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicket" ADD CONSTRAINT "IiosTicket_queueId_fkey" FOREIGN KEY ("queueId") REFERENCES "IiosSupportQueue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicket" ADD CONSTRAINT "IiosTicket_requesterActorId_fkey" FOREIGN KEY ("requesterActorId") REFERENCES "IiosActorRef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicket" ADD CONSTRAINT "IiosTicket_assignedActorId_fkey" FOREIGN KEY ("assignedActorId") REFERENCES "IiosActorRef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicket" ADD CONSTRAINT "IiosTicket_createdFromInteractionId_fkey" FOREIGN KEY ("createdFromInteractionId") REFERENCES "IiosInteraction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicketThreadLink" ADD CONSTRAINT "IiosTicketThreadLink_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "IiosTicket"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicketThreadLink" ADD CONSTRAINT "IiosTicketThreadLink_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "IiosThread"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosTicketStateHistory" ADD CONSTRAINT "IiosTicketStateHistory_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "IiosTicket"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosCallbackRequest" ADD CONSTRAINT "IiosCallbackRequest_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosCallbackRequest" ADD CONSTRAINT "IiosCallbackRequest_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "IiosTicket"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosCallbackRequest" ADD CONSTRAINT "IiosCallbackRequest_requesterActorId_fkey" FOREIGN KEY ("requesterActorId") REFERENCES "IiosActorRef"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -95,6 +95,32 @@ enum IiosInboxState {
|
||||
STALE
|
||||
}
|
||||
|
||||
enum IiosTicketState {
|
||||
NEW
|
||||
OPEN
|
||||
PENDING_CUSTOMER
|
||||
PENDING_INTERNAL
|
||||
RESOLVED
|
||||
CLOSED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum IiosTicketPriority {
|
||||
P0
|
||||
P1
|
||||
P2
|
||||
P3
|
||||
P4
|
||||
}
|
||||
|
||||
enum IiosCallbackStatus {
|
||||
PENDING
|
||||
SCHEDULED
|
||||
COMPLETED
|
||||
MISSED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
// ─── Kernel tables ────────────────────────────────────────────────
|
||||
|
||||
/// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null).
|
||||
@@ -114,6 +140,9 @@ model IiosScope {
|
||||
threads IiosThread[]
|
||||
interactions IiosInteraction[]
|
||||
inboxItems IiosInboxItem[]
|
||||
supportQueues IiosSupportQueue[]
|
||||
tickets IiosTicket[]
|
||||
callbacks IiosCallbackRequest[]
|
||||
|
||||
@@index([orgId, appId, tenantId])
|
||||
}
|
||||
@@ -157,6 +186,10 @@ model IiosActorRef {
|
||||
receipts IiosMessageReceipt[]
|
||||
unreadCounters IiosUnreadCounter[]
|
||||
inboxItemsOwned IiosInboxItem[]
|
||||
teamMemberships IiosSupportTeamMember[]
|
||||
ticketsRequested IiosTicket[] @relation("TicketRequester")
|
||||
ticketsAssigned IiosTicket[] @relation("TicketAssignee")
|
||||
callbacksRequested IiosCallbackRequest[]
|
||||
}
|
||||
|
||||
/// A configured channel surface (PORTAL in P1).
|
||||
@@ -200,6 +233,7 @@ model IiosThread {
|
||||
interactions IiosInteraction[]
|
||||
unreadCounters IiosUnreadCounter[]
|
||||
inboxItems IiosInboxItem[]
|
||||
ticketLinks IiosTicketThreadLink[]
|
||||
|
||||
@@index([scopeId, status])
|
||||
}
|
||||
@@ -245,6 +279,7 @@ model IiosInteraction {
|
||||
parts IiosMessagePart[]
|
||||
receipts IiosMessageReceipt[]
|
||||
inboxItems IiosInboxItem[]
|
||||
ticketsCreatedFrom IiosTicket[]
|
||||
|
||||
@@unique([scopeId, idempotencyKey])
|
||||
@@index([threadId, occurredAt])
|
||||
@@ -369,3 +404,113 @@ model IiosInboxItemStateHistory {
|
||||
|
||||
@@index([inboxItemId])
|
||||
}
|
||||
|
||||
// ─── P4: Support (specialization over message + inbox) ────────────
|
||||
|
||||
/// A support queue; agents (team members) are assigned tickets from it.
|
||||
model IiosSupportQueue {
|
||||
id String @id @default(cuid())
|
||||
scopeId String
|
||||
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
supportTier String @default("REGULAR")
|
||||
assignmentPolicy Json?
|
||||
enabled Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
members IiosSupportTeamMember[]
|
||||
tickets IiosTicket[]
|
||||
|
||||
@@index([scopeId])
|
||||
}
|
||||
|
||||
/// An agent's membership + capacity in a queue. Agent = an ActorRef that is a member.
|
||||
model IiosSupportTeamMember {
|
||||
queueId String
|
||||
queue IiosSupportQueue @relation(fields: [queueId], references: [id], onDelete: Cascade)
|
||||
actorId String
|
||||
actor IiosActorRef @relation(fields: [actorId], references: [id])
|
||||
role String @default("AGENT")
|
||||
maxActive Int @default(3)
|
||||
activeCount Int @default(0)
|
||||
availabilityState String @default("OFFLINE")
|
||||
|
||||
@@id([queueId, actorId])
|
||||
@@index([actorId])
|
||||
}
|
||||
|
||||
/// Support work object. Linked many-to-many to threads; the thread stays generic.
|
||||
model IiosTicket {
|
||||
id String @id @default(cuid())
|
||||
scopeId String
|
||||
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
||||
queueId String?
|
||||
queue IiosSupportQueue? @relation(fields: [queueId], references: [id])
|
||||
requesterActorId String?
|
||||
requesterActor IiosActorRef? @relation("TicketRequester", fields: [requesterActorId], references: [id])
|
||||
assignedActorId String?
|
||||
assignedActor IiosActorRef? @relation("TicketAssignee", fields: [assignedActorId], references: [id])
|
||||
subject String
|
||||
state IiosTicketState @default(NEW)
|
||||
priority IiosTicketPriority @default(P3)
|
||||
issueClass String?
|
||||
createdFromInteractionId String?
|
||||
createdFromInteraction IiosInteraction? @relation(fields: [createdFromInteractionId], references: [id])
|
||||
traceId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
metadata Json?
|
||||
|
||||
threadLinks IiosTicketThreadLink[]
|
||||
stateHistory IiosTicketStateHistory[]
|
||||
callbacks IiosCallbackRequest[]
|
||||
|
||||
@@index([scopeId, state])
|
||||
@@index([assignedActorId, state])
|
||||
}
|
||||
|
||||
/// M:N link between tickets and kernel threads (one thread ↔ many tickets, and vice-versa).
|
||||
model IiosTicketThreadLink {
|
||||
ticketId String
|
||||
ticket IiosTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
threadId String
|
||||
thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
||||
relationKind String @default("PRIMARY")
|
||||
linkedAt DateTime @default(now())
|
||||
|
||||
@@id([ticketId, threadId])
|
||||
@@index([threadId])
|
||||
}
|
||||
|
||||
/// Ticket state-transition audit.
|
||||
model IiosTicketStateHistory {
|
||||
id String @id @default(cuid())
|
||||
ticketId String
|
||||
ticket IiosTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
fromState IiosTicketState?
|
||||
toState IiosTicketState
|
||||
actorId String?
|
||||
reasonCode String?
|
||||
at DateTime @default(now())
|
||||
|
||||
@@index([ticketId])
|
||||
}
|
||||
|
||||
/// Callback request placeholder (the Meeting module is P8; meetingRef is a stub).
|
||||
model IiosCallbackRequest {
|
||||
id String @id @default(cuid())
|
||||
scopeId String
|
||||
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
||||
ticketId String?
|
||||
ticket IiosTicket? @relation(fields: [ticketId], references: [id])
|
||||
requesterActorId String
|
||||
requesterActor IiosActorRef @relation(fields: [requesterActorId], references: [id])
|
||||
preferChannel String
|
||||
preferredTime DateTime?
|
||||
notes String?
|
||||
status IiosCallbackStatus @default(PENDING)
|
||||
meetingRef String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([scopeId, status])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user