From 2141cea8dc8b1f08cef4a6db925aa906ac866006 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Wed, 1 Jul 2026 11:51:19 +0530 Subject: [PATCH] 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) --- packages/iios-contracts/src/events.ts | 2 + .../20260701062059_support_init/migration.sql | 155 ++++++++++++++++++ packages/iios-service/prisma/schema.prisma | 151 ++++++++++++++++- 3 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 packages/iios-service/prisma/migrations/20260701062059_support_init/migration.sql diff --git a/packages/iios-contracts/src/events.ts b/packages/iios-contracts/src/events.ts index aa180ff..6d0585c 100644 --- a/packages/iios-contracts/src/events.ts +++ b/packages/iios-contracts/src/events.ts @@ -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; diff --git a/packages/iios-service/prisma/migrations/20260701062059_support_init/migration.sql b/packages/iios-service/prisma/migrations/20260701062059_support_init/migration.sql new file mode 100644 index 0000000..dcc823f --- /dev/null +++ b/packages/iios-service/prisma/migrations/20260701062059_support_init/migration.sql @@ -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; diff --git a/packages/iios-service/prisma/schema.prisma b/packages/iios-service/prisma/schema.prisma index 1a8c484..eaf323a 100644 --- a/packages/iios-service/prisma/schema.prisma +++ b/packages/iios-service/prisma/schema.prisma @@ -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]) +}