- packages/database: Campaign, LeadImport, Lead, DialAttempt, CallDisposition, Callback, SuppressionEntry (agente.md secao 53) - packages/shared: normalizePhone (BR, E.164) com 7 testes unitarios - apps/api/src/campaigns: CRUD completo (todos os campos da secao 24) com maquina de estados validada (DRAFT/READY/RUNNING/PAUSED/DRAINING/ STOPPED/COMPLETED) — edicao bloqueada com campanha RUNNING - apps/api/src/leads: import CSV via streaming multipart (deteccao automatica de delimitador, mapeamento de coluna por header, validacao+ normalizacao+dedupe em lotes de 500, CSV de rejeitados com motivo, modo dryRun para preview) - apps/api/src/suppression: CRUD + import CSV da lista de bloqueio, remocao sempre exige motivo e e auditada, isSuppressed() pronto para o dialer-worker checar antes de originar - apps/api/src/dispositions: CRUD de disposicoes de chamada Testado ponta a ponta: campanha criada com defaults corretos, import de CSV real (3 validos/1 invalido/1 duplicado, contadores batendo), leads persistidos com telefone normalizado, transicoes de estado da campanha rejeitando movimentos invalidos (PAUSED->PAUSED = 400).
638 lines
21 KiB
Plaintext
638 lines
21 KiB
Plaintext
// Schema do domínio de aplicação do B2BCall. Vive no schema "public" do
|
|
// Postgres — nunca misturado com as tabelas do Asterisk Realtime (schema
|
|
// "asterisk", ver infrastructure/postgres/init/002-asterisk-realtime.sql).
|
|
//
|
|
// Modelado incrementalmente por fase (ver TODO.md): esta primeira migration
|
|
// cobre apenas autenticação, RBAC e auditoria (Fase 3). Demais entidades
|
|
// (agentes, troncos, filas, campanhas, leads, ...) chegam em migrations
|
|
// subsequentes, nunca alteração manual de schema.
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
name String
|
|
email String @unique
|
|
passwordHash String @map("password_hash")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
mustChangePassword Boolean @default(false) @map("must_change_password")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
roles UserRole[]
|
|
sessions Session[]
|
|
passwordResetTokens PasswordResetToken[]
|
|
auditLogs AuditLog[]
|
|
agent Agent?
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
refreshTokenHash String @map("refresh_token_hash")
|
|
userAgent String? @map("user_agent")
|
|
ipAddress String? @map("ip_address")
|
|
expiresAt DateTime @map("expires_at")
|
|
revokedAt DateTime? @map("revoked_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
@@map("sessions")
|
|
}
|
|
|
|
model PasswordResetToken {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
tokenHash String @unique @map("token_hash")
|
|
expiresAt DateTime @map("expires_at")
|
|
usedAt DateTime? @map("used_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@map("password_reset_tokens")
|
|
}
|
|
|
|
model Role {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
description String?
|
|
isSystem Boolean @default(false) @map("is_system")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
users UserRole[]
|
|
permissions RolePermission[]
|
|
|
|
@@map("roles")
|
|
}
|
|
|
|
model Permission {
|
|
id String @id @default(uuid())
|
|
key String @unique
|
|
description String?
|
|
|
|
roles RolePermission[]
|
|
|
|
@@map("permissions")
|
|
}
|
|
|
|
model UserRole {
|
|
userId String @map("user_id")
|
|
roleId String @map("role_id")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([userId, roleId])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model RolePermission {
|
|
roleId String @map("role_id")
|
|
permissionId String @map("permission_id")
|
|
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([roleId, permissionId])
|
|
@@map("role_permissions")
|
|
}
|
|
|
|
model AuditLog {
|
|
id BigInt @id @default(autoincrement())
|
|
userId String? @map("user_id")
|
|
action String
|
|
entityType String? @map("entity_type")
|
|
entityId String? @map("entity_id")
|
|
before Json?
|
|
after Json?
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId])
|
|
@@index([entityType, entityId])
|
|
@@index([createdAt])
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Fase 4 — Telefonia. CRUD da aplicação; os objetos PJSIP correspondentes
|
|
// (ps_endpoints/ps_auths/ps_aors/ps_endpoint_id_ips/ps_registrations) são
|
|
// provisionados no schema "asterisk" pelo TrunksService/ExtensionsService
|
|
// (packages/telephony), nunca editados manualmente.
|
|
// ===========================================================================
|
|
|
|
enum TrunkType {
|
|
IP
|
|
AUTH
|
|
REGISTRATION
|
|
}
|
|
|
|
enum DtmfMode {
|
|
rfc4733
|
|
info
|
|
inband
|
|
auto
|
|
}
|
|
|
|
model Trunk {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
type TrunkType
|
|
host String
|
|
port Int @default(5060)
|
|
transport String @default("udp")
|
|
username String?
|
|
// Segredo cifrado em repouso (AES-256-GCM, master key fora do banco —
|
|
// agente.md seção 55). Nunca retornado em claro pela API após salvar.
|
|
secretEncrypted String? @map("secret_encrypted")
|
|
fromUser String? @map("from_user")
|
|
fromDomain String? @map("from_domain")
|
|
contactUser String? @map("contact_user")
|
|
outboundProxy String? @map("outbound_proxy")
|
|
context String @default("outbound")
|
|
callerId String? @map("caller_id")
|
|
codecs String[] @default(["ulaw", "alaw"])
|
|
dtmfMode DtmfMode @default(rfc4733) @map("dtmf_mode")
|
|
qualifyFrequency Int @default(60) @map("qualify_frequency")
|
|
maxChannels Int? @map("max_channels")
|
|
maxCps Int @default(5) @map("max_cps")
|
|
allowedIps String[] @default([]) @map("allowed_ips")
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
campaigns Campaign[]
|
|
|
|
@@map("trunks")
|
|
}
|
|
|
|
model Extension {
|
|
id String @id @default(uuid())
|
|
number String @unique
|
|
name String
|
|
sipPasswordEncrypted String @map("sip_password_encrypted")
|
|
callerId String? @map("caller_id")
|
|
context String @default("b2bcall-agents")
|
|
codecs String[] @default(["ulaw", "alaw"])
|
|
transport String @default("udp")
|
|
maxContacts Int @default(1) @map("max_contacts")
|
|
qualifyFrequency Int @default(60) @map("qualify_frequency")
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("extensions")
|
|
}
|
|
|
|
// Último estado conhecido de cada ramal — alimentado por
|
|
// apps/asterisk-events a partir de DeviceStateChange/ContactStatus.
|
|
// Fonte do painel de monitoramento (nunca polling do Asterisk no frontend).
|
|
model ExtensionState {
|
|
extension String @id
|
|
deviceState String? @map("device_state")
|
|
contactStatus String? @map("contact_status")
|
|
contactUri String? @map("contact_uri")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("extension_states")
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Dialplan estruturado e versionado (agente.md seção 21). O modo "Advanced"
|
|
// citado na spec é apenas uma restrição de permissão no frontend sobre os
|
|
// mesmos dados — não um formato de armazenamento diferente.
|
|
// ===========================================================================
|
|
|
|
model DialplanEntry {
|
|
id String @id @default(uuid())
|
|
context String
|
|
exten String
|
|
priority Int
|
|
application String
|
|
argument String?
|
|
enabled Boolean @default(true)
|
|
order Int @default(0)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([context])
|
|
@@map("dialplan_entries")
|
|
}
|
|
|
|
enum DialplanVersionStatus {
|
|
APPLIED
|
|
FAILED
|
|
ROLLED_BACK
|
|
}
|
|
|
|
model DialplanVersion {
|
|
id String @id @default(uuid())
|
|
generatedConfig String @map("generated_config")
|
|
status DialplanVersionStatus
|
|
reloadResult String? @map("reload_result")
|
|
createdById String? @map("created_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("dialplan_versions")
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Fase 5 — Call Center. Usuário da aplicação (User) e agente de Call Center
|
|
// (Agent) são conceitos separados (agente.md seção 16): uma pessoa pode
|
|
// possuir os dois associados 1:1.
|
|
// ===========================================================================
|
|
|
|
enum QueueStrategy {
|
|
ringall
|
|
leastrecent
|
|
fewestcalls
|
|
random
|
|
rrmemory
|
|
rrordered
|
|
linear
|
|
wrandom
|
|
}
|
|
|
|
model Queue {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
number String @unique
|
|
strategy QueueStrategy @default(ringall)
|
|
timeout Int @default(15)
|
|
retry Int @default(5)
|
|
wrapUpTime Int @default(0) @map("wrap_up_time")
|
|
maxLen Int @default(0) @map("max_len")
|
|
musicOnHold String @default("default") @map("music_on_hold")
|
|
announce String?
|
|
serviceLevel Int @default(60) @map("service_level")
|
|
autoFill Boolean @default(true) @map("auto_fill")
|
|
ringInUse Boolean @default(true) @map("ring_in_use")
|
|
weight Int @default(0)
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
members QueueMember[]
|
|
campaigns Campaign[]
|
|
|
|
@@map("queues")
|
|
}
|
|
|
|
model QueueMember {
|
|
id String @id @default(uuid())
|
|
queueId String @map("queue_id")
|
|
agentId String @map("agent_id")
|
|
penalty Int @default(0)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
queue Queue @relation(fields: [queueId], references: [id], onDelete: Cascade)
|
|
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([queueId, agentId])
|
|
@@map("queue_members")
|
|
}
|
|
|
|
model PauseReason {
|
|
id String @id @default(uuid())
|
|
name String
|
|
code String @unique
|
|
description String?
|
|
maxDurationSeconds Int? @map("max_duration_seconds")
|
|
paid Boolean @default(false)
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
pauseEvents AgentPauseEvent[]
|
|
|
|
@@map("pause_reasons")
|
|
}
|
|
|
|
model Agent {
|
|
id String @id @default(uuid())
|
|
code String @unique
|
|
name String
|
|
userId String @unique @map("user_id")
|
|
active Boolean @default(true)
|
|
currentExtension String? @map("current_extension")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
sessions AgentSession[]
|
|
stateEvents AgentStateEvent[]
|
|
pauseEvents AgentPauseEvent[]
|
|
queues QueueMember[]
|
|
|
|
@@map("agents")
|
|
}
|
|
|
|
model AgentSession {
|
|
id String @id @default(uuid())
|
|
agentId String @map("agent_id")
|
|
extension String
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
endedAt DateTime? @map("ended_at")
|
|
|
|
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([agentId])
|
|
@@map("agent_sessions")
|
|
}
|
|
|
|
enum AgentState {
|
|
OFFLINE
|
|
LOGGED_IN
|
|
AVAILABLE
|
|
RINGING
|
|
IN_CALL
|
|
WRAP_UP
|
|
PAUSED
|
|
}
|
|
|
|
// Trilha de auditoria da máquina de estados do agente (agente.md seção 48).
|
|
// Sempre exatamente um registro "aberto" (endedAt = null) por agente.
|
|
model AgentStateEvent {
|
|
id String @id @default(uuid())
|
|
agentId String @map("agent_id")
|
|
state AgentState
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
endedAt DateTime? @map("ended_at")
|
|
|
|
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([agentId, endedAt])
|
|
@@map("agent_state_events")
|
|
}
|
|
|
|
model AgentPauseEvent {
|
|
id String @id @default(uuid())
|
|
agentId String @map("agent_id")
|
|
pauseReasonId String @map("pause_reason_id")
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
endedAt DateTime? @map("ended_at")
|
|
|
|
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
|
pauseReason PauseReason @relation(fields: [pauseReasonId], references: [id], onDelete: Restrict)
|
|
|
|
@@index([agentId, endedAt])
|
|
@@map("agent_pause_events")
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Fase 6 — Campanhas e Discador Preditivo. Núcleo mais crítico do sistema
|
|
// (agente.md seção 30). O Asterisk nunca é fonte de verdade para este
|
|
// domínio (seção 97) — todo o estado vive aqui.
|
|
// ===========================================================================
|
|
|
|
enum CampaignStatus {
|
|
DRAFT
|
|
READY
|
|
RUNNING
|
|
PAUSED
|
|
DRAINING
|
|
STOPPED
|
|
COMPLETED
|
|
}
|
|
|
|
model Campaign {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
description String?
|
|
queueId String @map("queue_id")
|
|
trunkId String @map("trunk_id")
|
|
callerId String? @map("caller_id")
|
|
context String @default("outbound")
|
|
status CampaignStatus @default(DRAFT)
|
|
|
|
startDate DateTime? @map("start_date")
|
|
endDate DateTime? @map("end_date")
|
|
// 0=domingo .. 6=sábado (ISO-like, mas com domingo em 0 por simplicidade
|
|
// de exibição em pt-BR). Vazio = todos os dias.
|
|
daysOfWeek Int[] @default([1, 2, 3, 4, 5]) @map("days_of_week")
|
|
startTime String @default("08:00") @map("start_time")
|
|
endTime String @default("20:00") @map("end_time")
|
|
timezone String @default("America/Sao_Paulo")
|
|
|
|
maxCps Int @default(2) @map("max_cps")
|
|
maxConcurrentCalls Int @default(10) @map("max_concurrent_calls")
|
|
pacingInitial Float @default(1.0) @map("pacing_initial")
|
|
pacingMin Float @default(0.5) @map("pacing_min")
|
|
pacingMax Float @default(3.0) @map("pacing_max")
|
|
targetAbandonRate Float @default(0.03) @map("target_abandon_rate")
|
|
maxWaitForAgentSeconds Int @default(30) @map("max_wait_for_agent_seconds")
|
|
ringTimeoutSeconds Int @default(25) @map("ring_timeout_seconds")
|
|
maxAttempts Int @default(5) @map("max_attempts")
|
|
// Minutos de espera até nova tentativa por causa de encerramento.
|
|
// Default reflete agente.md seção 79.
|
|
retryRules Json @default("{\"BUSY\":15,\"NO_ANSWER\":60,\"CONGESTION\":5,\"FAILED\":30}") @map("retry_rules")
|
|
|
|
amdEnabled Boolean @default(false) @map("amd_enabled")
|
|
wrapUpTimeSeconds Int @default(0) @map("wrap_up_time_seconds")
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
queue Queue @relation(fields: [queueId], references: [id], onDelete: Restrict)
|
|
trunk Trunk @relation(fields: [trunkId], references: [id], onDelete: Restrict)
|
|
leads Lead[]
|
|
imports LeadImport[]
|
|
attempts DialAttempt[]
|
|
callbacks Callback[]
|
|
|
|
@@map("campaigns")
|
|
}
|
|
|
|
enum LeadStatus {
|
|
NEW
|
|
READY
|
|
RESERVED
|
|
DIALING
|
|
RINGING
|
|
ANSWERED
|
|
CONNECTED_AGENT
|
|
BUSY
|
|
NO_ANSWER
|
|
FAILED
|
|
INVALID
|
|
VOICEMAIL
|
|
CALLBACK
|
|
COMPLETED
|
|
DO_NOT_CALL
|
|
MAX_ATTEMPTS
|
|
}
|
|
|
|
enum LeadImportStatus {
|
|
PROCESSING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
model LeadImport {
|
|
id String @id @default(uuid())
|
|
campaignId String @map("campaign_id")
|
|
filename String
|
|
status LeadImportStatus @default(PROCESSING)
|
|
totalRows Int @default(0) @map("total_rows")
|
|
validRows Int @default(0) @map("valid_rows")
|
|
invalidRows Int @default(0) @map("invalid_rows")
|
|
duplicateRows Int @default(0) @map("duplicate_rows")
|
|
rejectedCsv String? @map("rejected_csv")
|
|
createdById String? @map("created_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
leads Lead[]
|
|
|
|
@@map("lead_imports")
|
|
}
|
|
|
|
model Lead {
|
|
id String @id @default(uuid())
|
|
campaignId String @map("campaign_id")
|
|
importId String? @map("import_id")
|
|
name String?
|
|
phone String
|
|
phoneNormalized String @map("phone_normalized")
|
|
status LeadStatus @default(NEW)
|
|
attemptCount Int @default(0) @map("attempt_count")
|
|
lastAttemptAt DateTime? @map("last_attempt_at")
|
|
nextAttemptAt DateTime? @map("next_attempt_at")
|
|
lastResult String? @map("last_result")
|
|
reservedAt DateTime? @map("reserved_at")
|
|
reservedBy String? @map("reserved_by")
|
|
customFields Json? @map("custom_fields")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
import LeadImport? @relation(fields: [importId], references: [id], onDelete: SetNull)
|
|
attempts DialAttempt[]
|
|
callbacks Callback[]
|
|
|
|
@@index([campaignId, status, nextAttemptAt])
|
|
@@index([phoneNormalized])
|
|
@@map("leads")
|
|
}
|
|
|
|
enum CallState {
|
|
CREATED
|
|
RESERVED
|
|
ORIGINATING
|
|
RINGING
|
|
ANSWERED
|
|
QUEUED
|
|
AGENT_CONNECTED
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
enum AmdResult {
|
|
HUMAN
|
|
MACHINE
|
|
NOT_SURE
|
|
HANGUP
|
|
}
|
|
|
|
// Tentativa de discagem — também a "chamada" em si (agente.md seção 36:
|
|
// "criar chamadas como state machine"). attempt_id é o próprio id, nunca o
|
|
// UNIQUEID do Asterisk (seção 97: "não utilize UNIQUEID como PK de negócio").
|
|
model DialAttempt {
|
|
id String @id @default(uuid())
|
|
leadId String @map("lead_id")
|
|
campaignId String @map("campaign_id")
|
|
state CallState @default(CREATED)
|
|
asteriskUniqueId String? @unique @map("asterisk_unique_id")
|
|
asteriskLinkedId String? @map("asterisk_linked_id")
|
|
calledNumber String @map("called_number")
|
|
callerIdUsed String? @map("caller_id_used")
|
|
agentId String? @map("agent_id")
|
|
dispositionId String? @map("disposition_id")
|
|
dispositionNotes String? @map("disposition_notes")
|
|
amdResult AmdResult? @map("amd_result")
|
|
hangupCause String? @map("hangup_cause")
|
|
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
ringingAt DateTime? @map("ringing_at")
|
|
answeredAt DateTime? @map("answered_at")
|
|
queuedAt DateTime? @map("queued_at")
|
|
agentConnectedAt DateTime? @map("agent_connected_at")
|
|
endedAt DateTime? @map("ended_at")
|
|
|
|
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
disposition CallDisposition? @relation(fields: [dispositionId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([campaignId, state])
|
|
@@index([leadId])
|
|
@@map("dial_attempts")
|
|
}
|
|
|
|
enum DispositionAction {
|
|
NONE
|
|
CALLBACK
|
|
DO_NOT_CALL
|
|
}
|
|
|
|
model CallDisposition {
|
|
id String @id @default(uuid())
|
|
name String
|
|
code String @unique
|
|
description String?
|
|
action DispositionAction @default(NONE)
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
attempts DialAttempt[]
|
|
|
|
@@map("call_dispositions")
|
|
}
|
|
|
|
model Callback {
|
|
id String @id @default(uuid())
|
|
leadId String @map("lead_id")
|
|
campaignId String @map("campaign_id")
|
|
preferredAgentId String? @map("preferred_agent_id")
|
|
scheduledAt DateTime @map("scheduled_at")
|
|
timezone String @default("America/Sao_Paulo")
|
|
notes String?
|
|
completed Boolean @default(false)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([scheduledAt, completed])
|
|
@@map("call_callbacks")
|
|
}
|
|
|
|
model SuppressionEntry {
|
|
id String @id @default(uuid())
|
|
phoneNormalized String @unique @map("phone_normalized")
|
|
reason String?
|
|
addedById String? @map("added_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("suppression_list")
|
|
}
|