2141cea8dc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
517 lines
16 KiB
Plaintext
517 lines
16 KiB
Plaintext
// IIOS Interaction Kernel — P1 schema (native portal only).
|
|
// Lifted from the Bottom-Up Evolution Atlas §10 kernel DDL. ONLY kernel tables:
|
|
// no support / inbox / route / AI tables (those are earned in later phases).
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ─── Enums (one value per line) ───────────────────────────────────
|
|
enum IiosActorKind {
|
|
HUMAN
|
|
AI
|
|
SERVICE
|
|
BOT
|
|
UNKNOWN
|
|
}
|
|
|
|
enum IiosHandleKind {
|
|
PORTAL_USER
|
|
EMAIL
|
|
PHONE
|
|
WHATSAPP
|
|
SLACK
|
|
MATTERMOST
|
|
TELEGRAM
|
|
EXTERNAL_VISITOR
|
|
}
|
|
|
|
enum IiosInteractionKind {
|
|
MESSAGE
|
|
EMAIL
|
|
SYSTEM_NOTICE
|
|
INBOX_WORK
|
|
SUPPORT_CASE
|
|
MEETING_REQUEST
|
|
DIGEST
|
|
SUMMARY
|
|
NOTIFICATION
|
|
}
|
|
|
|
enum IiosMessagePartKind {
|
|
TEXT
|
|
HTML
|
|
MARKDOWN
|
|
MEDIA_REF
|
|
FILE_REF
|
|
VOICE_REF
|
|
LOCATION
|
|
STRUCTURED_JSON
|
|
}
|
|
|
|
enum IiosDeliveryState {
|
|
RECEIVED
|
|
NORMALIZED
|
|
POLICY_HELD
|
|
STORED
|
|
DELIVERED
|
|
READ
|
|
FAILED
|
|
DELETED_LOCAL
|
|
REDACTED
|
|
}
|
|
|
|
enum IiosReceiptKind {
|
|
DELIVERED
|
|
READ
|
|
ACKED
|
|
FAILED
|
|
SUPPRESSED
|
|
RATE_LIMITED
|
|
}
|
|
|
|
enum IiosInboxItemKind {
|
|
NEEDS_REPLY
|
|
NEEDS_REVIEW
|
|
NEEDS_APPROVAL
|
|
SUPPORT_UPDATE
|
|
MEETING_FOLLOWUP
|
|
DIGEST
|
|
SYSTEM_ALERT
|
|
CRM_OWNER_INTEREST
|
|
}
|
|
|
|
enum IiosInboxState {
|
|
OPEN
|
|
SNOOZED
|
|
DONE
|
|
ARCHIVED
|
|
CANCELLED
|
|
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).
|
|
model IiosScope {
|
|
id String @id @default(cuid())
|
|
orgId String
|
|
appId String
|
|
buId String?
|
|
tenantId String?
|
|
workspaceId String?
|
|
projectId String?
|
|
userScopeId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
sourceHandles IiosSourceHandle[]
|
|
channels IiosChannel[]
|
|
threads IiosThread[]
|
|
interactions IiosInteraction[]
|
|
inboxItems IiosInboxItem[]
|
|
supportQueues IiosSupportQueue[]
|
|
tickets IiosTicket[]
|
|
callbacks IiosCallbackRequest[]
|
|
|
|
@@index([orgId, appId, tenantId])
|
|
}
|
|
|
|
/// A channel-specific handle (phone/email/portal user). NOT identity — stays
|
|
/// UNVERIFIED until MDM resolves it; no silent merge.
|
|
model IiosSourceHandle {
|
|
id String @id @default(cuid())
|
|
scopeId String
|
|
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
|
kind IiosHandleKind
|
|
externalId String
|
|
displayName String?
|
|
canonicalEntityId String? // set only when MDM resolves; null = unresolved
|
|
confidence Float @default(0)
|
|
verificationState String @default("UNVERIFIED")
|
|
firstSeenAt DateTime @default(now())
|
|
lastSeenAt DateTime @default(now())
|
|
metadata Json?
|
|
|
|
actors IiosActorRef[]
|
|
|
|
@@unique([scopeId, kind, externalId])
|
|
@@index([canonicalEntityId])
|
|
}
|
|
|
|
/// The local actor (human / AI / service / unknown) behind an interaction.
|
|
model IiosActorRef {
|
|
id String @id @default(cuid())
|
|
kind IiosActorKind
|
|
sourceHandleId String?
|
|
sourceHandle IiosSourceHandle? @relation(fields: [sourceHandleId], references: [id])
|
|
platformPrincipalRef String?
|
|
canonicalEntityId String?
|
|
displayName String?
|
|
createdAt DateTime @default(now())
|
|
|
|
threadsCreated IiosThread[] @relation("ThreadCreatedBy")
|
|
participations IiosThreadParticipant[]
|
|
interactions IiosInteraction[]
|
|
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).
|
|
model IiosChannel {
|
|
id String @id @default(cuid())
|
|
scopeId String
|
|
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
|
channelType String
|
|
channelName String
|
|
direction String @default("BOTH")
|
|
capabilityContract Json?
|
|
externalRef String?
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
threads IiosThread[]
|
|
interactions IiosInteraction[]
|
|
|
|
@@index([scopeId, channelType])
|
|
}
|
|
|
|
/// A conversation/thread on a channel.
|
|
model IiosThread {
|
|
id String @id @default(cuid())
|
|
scopeId String
|
|
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
|
channelId String?
|
|
channel IiosChannel? @relation(fields: [channelId], references: [id])
|
|
threadKind String @default("CONVERSATION")
|
|
subject String?
|
|
externalThreadRef String?
|
|
status String @default("OPEN")
|
|
visibilityClass String @default("PRIVATE")
|
|
createdByActorId String?
|
|
createdByActor IiosActorRef? @relation("ThreadCreatedBy", fields: [createdByActorId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
metadata Json?
|
|
|
|
participants IiosThreadParticipant[]
|
|
interactions IiosInteraction[]
|
|
unreadCounters IiosUnreadCounter[]
|
|
inboxItems IiosInboxItem[]
|
|
ticketLinks IiosTicketThreadLink[]
|
|
|
|
@@index([scopeId, status])
|
|
}
|
|
|
|
model IiosThreadParticipant {
|
|
threadId String
|
|
thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
|
actorId String
|
|
actor IiosActorRef @relation(fields: [actorId], references: [id])
|
|
participantRole String @default("MEMBER")
|
|
joinedAt DateTime @default(now())
|
|
leftAt DateTime?
|
|
|
|
@@id([threadId, actorId])
|
|
}
|
|
|
|
/// The semantic envelope. Idempotent per (scope, idempotencyKey).
|
|
model IiosInteraction {
|
|
id String @id @default(cuid())
|
|
scopeId String
|
|
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
|
kind IiosInteractionKind
|
|
threadId String?
|
|
thread IiosThread? @relation(fields: [threadId], references: [id])
|
|
channelId String?
|
|
channel IiosChannel? @relation(fields: [channelId], references: [id])
|
|
actorId String?
|
|
actor IiosActorRef? @relation(fields: [actorId], references: [id])
|
|
parentInteractionId String?
|
|
parent IiosInteraction? @relation("InteractionParent", fields: [parentInteractionId], references: [id])
|
|
children IiosInteraction[] @relation("InteractionParent")
|
|
externalId String?
|
|
idempotencyKey String
|
|
occurredAt DateTime @default(now())
|
|
receivedAt DateTime @default(now())
|
|
status IiosDeliveryState @default(RECEIVED)
|
|
policyDecisionRef String?
|
|
consentReceiptRef String?
|
|
crreBundleRef String?
|
|
traceId String?
|
|
metadata Json?
|
|
|
|
parts IiosMessagePart[]
|
|
receipts IiosMessageReceipt[]
|
|
inboxItems IiosInboxItem[]
|
|
ticketsCreatedFrom IiosTicket[]
|
|
|
|
@@unique([scopeId, idempotencyKey])
|
|
@@index([threadId, occurredAt])
|
|
}
|
|
|
|
model IiosMessagePart {
|
|
id String @id @default(cuid())
|
|
interactionId String
|
|
interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade)
|
|
partIndex Int
|
|
kind IiosMessagePartKind
|
|
bodyText String?
|
|
contentRef String?
|
|
mimeType String?
|
|
sizeBytes BigInt?
|
|
checksumSha256 String?
|
|
metadata Json?
|
|
|
|
@@unique([interactionId, partIndex])
|
|
}
|
|
|
|
/// Transactional outbox — written in the same tx as the business row.
|
|
model IiosOutboxEvent {
|
|
eventId String @id @default(cuid())
|
|
aggregateType String
|
|
aggregateId String
|
|
eventType String
|
|
cloudEvent Json
|
|
partitionKey String
|
|
status String @default("PENDING")
|
|
attempts Int @default(0)
|
|
nextAttemptAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
publishedAt DateTime?
|
|
|
|
@@index([status, nextAttemptAt, createdAt])
|
|
}
|
|
|
|
/// Idempotent-consumer ledger for the outbox relay / downstream consumers.
|
|
model IiosProcessedEvent {
|
|
consumerName String
|
|
eventId String
|
|
processedAt DateTime @default(now())
|
|
resultHash String?
|
|
|
|
@@id([consumerName, eventId])
|
|
}
|
|
|
|
// ─── P2: messaging — receipts & unread ────────────────────────────
|
|
|
|
/// Delivery/read receipt for a message interaction, per actor.
|
|
model IiosMessageReceipt {
|
|
id String @id @default(cuid())
|
|
interactionId String
|
|
interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade)
|
|
actorId String?
|
|
actor IiosActorRef? @relation(fields: [actorId], references: [id])
|
|
receiptKind IiosReceiptKind
|
|
occurredAt DateTime @default(now())
|
|
providerReceiptRef String?
|
|
metadata Json?
|
|
|
|
@@unique([interactionId, actorId, receiptKind])
|
|
@@index([interactionId])
|
|
}
|
|
|
|
/// Per-(thread, actor) unread count + last-read marker.
|
|
model IiosUnreadCounter {
|
|
threadId String
|
|
thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
|
actorId String
|
|
actor IiosActorRef @relation(fields: [actorId], references: [id])
|
|
unreadCount Int @default(0)
|
|
lastReadInteractionId String?
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([threadId, actorId])
|
|
}
|
|
|
|
// ─── P3: Inbox (work/awareness surface) ───────────────────────────
|
|
|
|
/// A durable action/awareness item owned by one actor. Fed by the event
|
|
/// projector (deterministic, collapse-per-thread). NOT a thin unread projection.
|
|
model IiosInboxItem {
|
|
id String @id @default(cuid())
|
|
scopeId String
|
|
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
|
ownerActorId String
|
|
ownerActor IiosActorRef @relation(fields: [ownerActorId], references: [id])
|
|
kind IiosInboxItemKind
|
|
state IiosInboxState @default(OPEN)
|
|
title String
|
|
summary String?
|
|
priority String @default("NORMAL")
|
|
dueAt DateTime?
|
|
sourceInteractionId String?
|
|
sourceInteraction IiosInteraction? @relation(fields: [sourceInteractionId], references: [id])
|
|
threadId String?
|
|
thread IiosThread? @relation(fields: [threadId], references: [id])
|
|
traceId String?
|
|
actionRef Json?
|
|
policyDecisionRef String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
metadata Json?
|
|
|
|
stateHistory IiosInboxItemStateHistory[]
|
|
|
|
@@index([ownerActorId, state, priority])
|
|
}
|
|
|
|
/// Audit trail of inbox-item state transitions.
|
|
model IiosInboxItemStateHistory {
|
|
id String @id @default(cuid())
|
|
inboxItemId String
|
|
inboxItem IiosInboxItem @relation(fields: [inboxItemId], references: [id], onDelete: Cascade)
|
|
fromState IiosInboxState?
|
|
toState IiosInboxState
|
|
actorId String?
|
|
reasonCode String?
|
|
at DateTime @default(now())
|
|
|
|
@@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])
|
|
}
|