Files
2026-06-09 02:02:40 +05:30

436 lines
12 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================================================
// Tenancy
// ============================================================================
model Tenant {
id String @id @default(cuid())
slug String @unique
name String
isActive Boolean @default(true)
isForwardingPaused Boolean @default(false)
settings Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
admins Admin[]
tenantBots TenantBot[]
groups Group[]
messages Message[]
approvals Approval[]
syncRoutes SyncRoute[]
consentRecords ConsentRecord[]
memberOptOuts MemberOptOut[]
towerUsers TowerUser[]
auditEvents AuditEvent[]
rules TenantRule[]
groupAccesses GroupAccess[]
}
enum AdminRole {
OWNER
ADMIN
VIEWER
}
model Admin {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
email String
passwordHash String
role AdminRole @default(ADMIN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
claimedGroups Group[] @relation("claimer")
@@unique([tenantId, email])
@@index([tenantId])
}
model SuperAdmin {
id String @id @default(cuid())
email String @unique
passwordHash String
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum ActorType {
ADMIN
SYSTEM
ADAPTER
MEMBER
}
model AuditEvent {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
actorType ActorType
actorId String?
action String
resourceType String
resourceId String
payload Json @default("{}")
traceId String?
createdAt DateTime @default(now())
@@index([tenantId, createdAt])
@@index([resourceType, resourceId])
}
// ============================================================================
// WhatsApp accounts (Phase 2B: bots only, tenant-less, accessed via TenantBot)
// ============================================================================
enum AccountStatus {
ACTIVE
DISCONNECTED
BANNED
PAIRING
}
model Account {
id String @id @default(cuid())
// tenantId REMOVED in Phase 2B — bots are global, access scoped via TenantBot
platform String
jid String
sessionPath String
displayName String?
status AccountStatus @default(ACTIVE)
qrCode String?
isBot Boolean @default(true)
pairingToken String? @unique
pairingExpiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenants TenantBot[]
groups Group[]
@@unique([platform, jid])
@@index([isBot])
@@index([status])
}
// Many-to-many: which tenants may claim groups from which bot.
// Phase 2B ships with implicit "all tenants" (UI auto-grants on first claim),
// but the table is wired so multi-bot + restricted sharing works in later phases.
model TenantBot {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
accountId String
account Account @relation(fields: [accountId], references: [id])
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@unique([tenantId, accountId])
@@index([accountId])
}
// ============================================================================
// Groups + claim lifecycle
// ============================================================================
enum GroupClaimStatus {
PENDING_CLAIM
CLAIMED
RELEASED
EXPIRED
}
model Group {
id String @id @default(cuid())
// tenantId nullable: null while PENDING_CLAIM/EXPIRED, set once CLAIMED
tenantId String?
tenant Tenant? @relation(fields: [tenantId], references: [id])
platform String
platformId String
name String
description String?
isActive Boolean @default(true)
accountId String?
account Account? @relation(fields: [accountId], references: [id])
claimStatus GroupClaimStatus @default(PENDING_CLAIM)
claimedAt DateTime?
claimedByAdminId String?
claimedByAdmin Admin? @relation("claimer", fields: [claimedByAdminId], references: [id])
claimExpiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[]
syncRoutesFrom SyncRoute[] @relation("sourceGroup")
syncRoutesTo SyncRoute[] @relation("targetGroup")
consents ConsentRecord[]
claimTokens GroupClaimToken[]
groupAccesses GroupAccess[]
@@unique([platform, platformId])
@@index([accountId])
@@index([tenantId])
@@index([claimStatus])
}
// ============================================================================
// Message ingest + approval
// ============================================================================
model Message {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
platform String
platformMsgId String
sourceGroupId String
sourceGroup Group @relation(fields: [sourceGroupId], references: [id])
senderJid String
senderName String?
senderTowerUserId String?
senderTowerUser TowerUser? @relation("senderTowerUser", fields: [senderTowerUserId], references: [id])
content String
mediaUrl String?
tags String[]
status MessageStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
approval Approval?
@@unique([platform, platformMsgId])
@@index([tenantId])
@@index([senderTowerUserId])
}
enum MessageStatus {
PENDING
APPROVED
REJECTED
DISTRIBUTED
ARCHIVED
}
model Approval {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
messageId String @unique
message Message @relation(fields: [messageId], references: [id])
adminId String
decision ApprovalDecision
notes String?
decidedAt DateTime @default(now())
@@index([tenantId])
}
enum ApprovalDecision {
APPROVED
REJECTED
}
model SyncRoute {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
sourceGroupId String
sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id])
targetGroupId String
targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id])
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@unique([sourceGroupId, targetGroupId])
@@index([tenantId])
}
// ============================================================================
// Group claiming + sharing
// ============================================================================
model GroupClaimToken {
id String @id @default(cuid())
groupId String
group Group @relation(fields: [groupId], references: [id])
token String @unique
creatorJid String
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
@@index([expiresAt])
}
model GroupAccess {
id String @id @default(cuid())
groupId String
group Group @relation(fields: [groupId], references: [id])
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
grantedBy String
createdAt DateTime @default(now())
@@unique([groupId, tenantId])
@@index([tenantId])
}
// ============================================================================
// Member onboarding (Phase 2B)
// ============================================================================
enum ConsentScope {
INGEST
ARCHIVE
REPLICATE
DISPLAY
}
enum ConsentStatus {
GRANTED
REVOKED
}
enum MemberOptOutReason {
STOP_KEYWORD
SELF_PORTAL
ADMIN_ACTION
}
// Hashed identity: SHA-256 of E.164 phone number (pepper via JWT_SECRET).
model TowerUser {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
phoneHash String
jid String
displayName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
consents ConsentRecord[]
optOuts MemberOptOut[]
sessions TowerSession[]
messages Message[] @relation("senderTowerUser")
@@unique([tenantId, phoneHash])
@@index([phoneHash])
@@index([tenantId])
@@index([jid])
}
model TowerSession {
id String @id @default(cuid())
userId String
user TowerUser @relation(fields: [userId], references: [id])
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
}
model ConsentRecord {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
groupId String
group Group @relation(fields: [groupId], references: [id])
userId String
user TowerUser @relation(fields: [userId], references: [id])
scopes ConsentScope[]
retentionDays Int @default(90)
policyVersion String
status ConsentStatus @default(GRANTED)
proofEventId String
effectiveAt DateTime @default(now())
revokedAt DateTime?
createdAt DateTime @default(now())
@@index([tenantId, groupId, userId])
@@index([status])
@@index([userId])
}
model MemberOptOut {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
userId String
user TowerUser @relation(fields: [userId], references: [id])
groupId String?
reason MemberOptOutReason
notes String?
createdAt DateTime @default(now())
@@index([tenantId, userId])
@@index([groupId])
@@index([userId])
}
model OtpChallenge {
id String @id @default(cuid())
tenantId String
jid String
phoneHash String
code String
scopes ConsentScope[]
retentionDays Int @default(90)
policyVersion String
groupId String
expiresAt DateTime
consumedAt DateTime?
sentAt DateTime?
createdAt DateTime @default(now())
@@index([tenantId, jid])
@@index([expiresAt])
@@index([sentAt])
}
// ============================================================================
// Tenant Rules Engine
// ============================================================================
enum RuleMatchType {
HASHTAG
PREFIX
REACTION_EMOJI
}
enum RuleAction {
FLAG
AUTO_APPROVE
SKIP
REJECT
}
model TenantRule {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
matchType RuleMatchType
matchValue String
action RuleAction
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, matchType, matchValue])
@@index([tenantId, isActive])
@@index([tenantId, matchType])
}