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
+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])
}