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:
2026-07-01 11:51:19 +05:30
parent 942f40292e
commit 2141cea8dc
3 changed files with 305 additions and 3 deletions
+2
View File
@@ -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;
+148 -3
View File
@@ -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])
}
@@ -242,9 +276,10 @@ model IiosInteraction {
traceId String?
metadata Json?
parts IiosMessagePart[]
receipts IiosMessageReceipt[]
inboxItems IiosInboxItem[]
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])
}