generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } enum TenantStatus { TRIAL ACTIVE SUSPENDED PAST_DUE CANCELLED @@map("tenant_status") } model Tenant { id String @id @default(uuid()) @db.Uuid code String @unique slug String @unique legalName String @map("legal_name") tradeName String? @map("trade_name") taxId String? @map("tax_id") status TenantStatus @default(TRIAL) timezone String @default("America/Sao_Paulo") locale String @default("pt-BR") billingCurrency String @default("BRL") @map("billing_currency") telephonyDomain String? @map("telephony_domain") planId String @map("plan_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") plan Plan @relation(fields: [planId], references: [id]) memberships TenantMembership[] userRoles UserRole[] extensions Extension[] trunks Trunk[] dialplanExtensions DialplanExtension[] dialplanVersions DialplanVersion[] queues Queue[] agents Agent[] pauseReasons PauseReason[] tiers Tier[] agentSessions AgentSession[] agentStateEvents AgentStateEvent[] agentPauseEvents AgentPauseEvent[] campaigns Campaign[] leads Lead[] suppressionEntries SuppressionEntry[] callAttempts CallAttempt[] campaignStats CampaignStats[] @@map("tenants") } // Entitlements por plano (agente.md secao 56) — "criar sistema genérico... // não espalhar `if plan == PRO` pelo código". Todo tenant tem exatamente um // Plan (nunca null: em vez de checar "tem plano?", o código só lê o limite // e trata null-no-campo como "sem limite", agente.md secao 57-61). model Plan { id String @id @default(uuid()) @db.Uuid key String @unique name String maxExtensions Int? @map("max_extensions") maxAgents Int? @map("max_agents") maxTrunks Int? @map("max_trunks") maxQueues Int? @map("max_queues") maxCampaigns Int? @map("max_campaigns") maxCps Int? @map("max_cps") maxConcurrentCalls Int? @map("max_concurrent_calls") maxDailyCalls Int? @map("max_daily_calls") maxMonthlyCalls Int? @map("max_monthly_calls") maxRecordingStorageGb Int? @map("max_recording_storage_gb") recordingEnabled Boolean @default(true) @map("recording_enabled") aiEnabled Boolean @default(false) @map("ai_enabled") aiTranscriptionEnabled Boolean @default(false) @map("ai_transcription_enabled") aiAnalysisEnabled Boolean @default(false) @map("ai_analysis_enabled") apiAccessEnabled Boolean @default(true) @map("api_access_enabled") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") tenants Tenant[] @@map("plans") } enum UserStatus { ACTIVE DISABLED @@map("user_status") } // Identidade global do usuário. NUNCA carrega tenant_id diretamente — o tenant // é sempre resolvido via TenantMembership (agente.md secao 31). model User { id String @id @default(uuid()) @db.Uuid email String @unique passwordHash String @map("password_hash") name String status UserStatus @default(ACTIVE) mustChangePassword Boolean @default(false) @map("must_change_password") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") memberships TenantMembership[] userRoles UserRole[] sessions Session[] agents Agent[] @@map("users") } enum RoleScope { PLATFORM TENANT @@map("role_scope") } // Definições de role. Roles de sistema (isSystem=true) são criadas pelo seed // (agente.md secao 142) e não podem ser removidas via API. model Role { id String @id @default(uuid()) @db.Uuid key String @unique name String scope RoleScope isSystem Boolean @default(false) @map("is_system") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") rolePermissions RolePermission[] userRoles UserRole[] @@map("roles") } model Permission { id String @id @default(uuid()) @db.Uuid key String @unique description String? rolePermissions RolePermission[] @@map("permissions") } model RolePermission { roleId String @map("role_id") @db.Uuid permissionId String @map("permission_id") @db.Uuid role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) @@id([roleId, permissionId]) @@map("role_permissions") } // tenantId é obrigatório quando role.scope == TENANT e deve ser nulo quando // role.scope == PLATFORM (invariante aplicado em packages/auth, não no banco — // ver docs/AUTHENTICATION.md). model UserRole { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid roleId String @map("role_id") @db.Uuid tenantId String? @map("tenant_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@unique([userId, roleId, tenantId]) @@map("user_roles") } // Refresh-token de longa duração (rotacionado a cada uso). Nunca guardamos o // token em texto puro — só o hash (SHA-256) usado para lookup/comparação. model Session { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid refreshTokenHash String @unique @map("refresh_token_hash") activeTenantId String? @map("active_tenant_id") @db.Uuid userAgent String? @map("user_agent") ipAddress String? @map("ip_address") createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime @map("expires_at") revokedAt DateTime? @map("revoked_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("sessions") } // tenantId nulo = evento de escopo plataforma (agente.md secao 150). model AuditLog { id String @id @default(uuid()) @db.Uuid tenantId String? @map("tenant_id") @db.Uuid userId String? @map("user_id") @db.Uuid 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") @@index([tenantId, createdAt]) @@index([userId]) @@map("audit_logs") } // Tabela tenant-scoped protegida por Row Level Security (ver migration // 'tenant_isolation' e docs/TENANT_ISOLATION.md). model TenantMembership { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid userId String @map("user_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") tenant Tenant @relation(fields: [tenantId], references: [id]) user User @relation(fields: [userId], references: [id]) @@unique([tenantId, userId]) @@index([tenantId]) @@map("tenant_memberships") } // Tabela tenant-scoped protegida por Row Level Security. sipPasswordEnc // guarda a senha SIP cifrada (AES-256-GCM, ver packages/shared/src/crypto.ts) // — nunca texto puro (agente.md secao 178). model Extension { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid number String name String domain String sipPasswordEnc String @map("sip_password_enc") callerIdName String? @map("caller_id_name") callerIdNumber String? @map("caller_id_number") context String @default("default") sofiaProfile String @default("internal") @map("sofia_profile") codecs String @default("PCMU,PCMA,OPUS") @map("codecs") maxRegistrations Int @default(1) @map("max_registrations") enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) agents Agent[] @@unique([tenantId, number]) @@index([tenantId]) @@map("extensions") } enum TrunkStatus { UP DOWN REGISTERED TRYING FAILED UNREGISTERED UNKNOWN @@map("trunk_status") } enum DtmfMode { RFC2833 INFO INBAND @@map("dtmf_mode") } enum SipTransport { UDP TCP TLS @@map("sip_transport") } // Tabela tenant-scoped protegida por Row Level Security. passwordEnc guarda // a senha do tronco cifrada (AES-256-GCM), como sipPasswordEnc em Extension // (agente.md secao 41, 178). model Trunk { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid name String description String? sofiaProfile String @default("external") @map("sofia_profile") host String proxy String? realm String? register Boolean @default(true) username String? passwordEnc String? @map("password_enc") fromUser String? @map("from_user") fromDomain String? @map("from_domain") registerProxy String? @map("register_proxy") outboundProxy String? @map("outbound_proxy") expireSeconds Int @default(3600) @map("expire_seconds") retrySeconds Int @default(30) @map("retry_seconds") callerIdName String? @map("caller_id_name") callerIdNumber String? @map("caller_id_number") codecs String @default("PCMU,PCMA,OPUS") dtmfMode DtmfMode @default(RFC2833) @map("dtmf_mode") ping Boolean @default(true) pingFrequency Int @default(30) @map("ping_frequency") transport SipTransport @default(UDP) inboundContext String @default("default") @map("inbound_context") maxCps Int? @map("max_cps") maxChannels Int? @map("max_channels") enabled Boolean @default(true) // Refletido pelos eventos sofia::gateway_state (agente.md secao 42) — // b2bcall-fs-events atualiza isso, nunca escrito manualmente pela API. status TrunkStatus @default(UNKNOWN) statusUpdatedAt DateTime? @map("status_updated_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) campaigns Campaign[] @@unique([tenantId, name]) @@index([tenantId]) @@map("trunks") } enum DialplanVersionStatus { DRAFT ACTIVE SUPERSEDED @@map("dialplan_version_status") } // Editor estruturado (agente.md secao 43) — fonte editável. As versões // publicadas (DialplanVersion) são um snapshot gerado a partir destas // linhas, não o que o FreeSWITCH consulta diretamente. model DialplanExtension { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid context String @default("default") name String conditionField String @map("condition_field") conditionExpr String @map("condition_expr") // Array de { application, data } — validado contra uma allowlist de // applications seguras na camada de API (agente.md secao 180: nunca // deixar input de usuário virar comando arbitrário no FreeSWITCH). actions Json antiActions Json? @map("anti_actions") continueOnFalse Boolean @default(false) @map("continue_on_false") order Int @default(0) enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) @@index([tenantId, context]) @@map("dialplan_extensions") } // Versionamento (agente.md secao 44): gerar -> validar -> versionar -> // ativar -> reloadxml -> verificar -> rollback se necessário. "Ativar" uma // versão anterior é o próprio mecanismo de rollback — não existe endpoint // separado. model DialplanVersion { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid context String version Int generatedXml String @map("generated_xml") status DialplanVersionStatus @default(DRAFT) createdByUserId String? @map("created_by_user_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") activatedAt DateTime? @map("activated_at") tenant Tenant @relation(fields: [tenantId], references: [id]) @@unique([tenantId, context, version]) @@index([tenantId, context, status]) @@map("dialplan_versions") } enum QueueStrategy { LONGEST_IDLE_AGENT ROUND_ROBIN TOP_DOWN AGENT_WITH_LEAST_TALK_TIME AGENT_WITH_FEWEST_CALLS SEQUENTIALLY_BY_AGENT_ORDER RING_ALL RING_PROGRESSIVELY @@map("queue_strategy") } // Tabela tenant-scoped protegida por RLS (agente.md secao 50-51). O nome no // FreeSWITCH é `@` — mod_callcenter usa // um namespace unico compartilhado entre tenants (nao ha equivalente ao // diretorio por-arquivo do Sofia pra isolar por tenant, ver docs/QUEUES.md). model Queue { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid name String description String? strategy QueueStrategy @default(LONGEST_IDLE_AGENT) mohSound String? @map("moh_sound") announceSound String? @map("announce_sound") announceFrequency Int @default(0) @map("announce_frequency") maxWaitTime Int @default(0) @map("max_wait_time") maxWaitTimeWithNoAgent Int @default(0) @map("max_wait_time_with_no_agent") agentNoAnswerStatus String? @map("agent_no_answer_status") tierRulesApply Boolean @default(false) @map("tier_rules_apply") tierRuleWaitSecond Int @default(300) @map("tier_rule_wait_second") discardAbandonedAfter Int @default(60) @map("discard_abandoned_after") abandonedResumeAllowed Boolean @default(false) @map("abandoned_resume_allowed") skipAgentsWithExternalCalls Boolean @default(true) @map("skip_agents_with_external_calls") recordingEnabled Boolean @default(false) @map("recording_enabled") enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) tiers Tier[] campaigns Campaign[] @@unique([tenantId, name]) @@index([tenantId]) @@map("queues") } enum AgentState { OFFLINE LOGGED_IN AVAILABLE RESERVED RINGING IN_CALL WRAP_UP PAUSED @@map("agent_state") } // Tabela tenant-scoped protegida por RLS. Separa User (login) / Agent // (identidade de call center) / Extension (ramal SIP usado como contato) — // agente.md secao 45. O nome no FreeSWITCH é `@`, // mesma convenção UUID das outras entidades (agente.md secao 47). model Agent { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid userId String @map("user_id") @db.Uuid extensionId String? @map("extension_id") @db.Uuid name String maxNoAnswer Int @default(3) @map("max_no_answer") wrapUpTime Int @default(10) @map("wrap_up_time") rejectDelayTime Int @default(10) @map("reject_delay_time") busyDelayTime Int @default(60) @map("busy_delay_time") noAnswerDelayTime Int @default(10) @map("no_answer_delay_time") // Espelha o estado real (secao 46) — nunca escrito diretamente pela API, // só por login/logout/pause/resume ou por eventos do FreeSWITCH. state AgentState @default(OFFLINE) stateUpdatedAt DateTime? @map("state_updated_at") enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) user User @relation(fields: [userId], references: [id]) extension Extension? @relation(fields: [extensionId], references: [id]) tiers Tier[] sessions AgentSession[] stateEvents AgentStateEvent[] pauseEvents AgentPauseEvent[] callAttempts CallAttempt[] @@unique([tenantId, userId]) @@index([tenantId]) @@map("agents") } // Queue <-> Agent (agente.md secao 52). O nome da fila/agente no FreeSWITCH // já é o "@" — level/position espelham 1:1 os mesmos conceitos // do mod_callcenter. model Tier { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid queueId String @map("queue_id") @db.Uuid agentId String @map("agent_id") @db.Uuid level Int @default(1) position Int @default(1) createdAt DateTime @default(now()) @map("created_at") tenant Tenant @relation(fields: [tenantId], references: [id]) queue Queue @relation(fields: [queueId], references: [id]) agent Agent @relation(fields: [agentId], references: [id]) @@unique([queueId, agentId]) @@index([tenantId]) @@map("tiers") } // Uma "sessão" = do login até o logout do agente (agente.md secao 45, 47). model AgentSession { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid agentId String @map("agent_id") @db.Uuid startedAt DateTime @default(now()) @map("started_at") endedAt DateTime? @map("ended_at") tenant Tenant @relation(fields: [tenantId], references: [id]) agent Agent @relation(fields: [agentId], references: [id]) @@index([tenantId, agentId]) @@map("agent_sessions") } // Histórico de transições de estado (agente.md secao 46) — nunca deletado, // serve de auditoria e insumo pra relatórios (secao 158). model AgentStateEvent { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid agentId String @map("agent_id") @db.Uuid state AgentState occurredAt DateTime @default(now()) @map("occurred_at") tenant Tenant @relation(fields: [tenantId], references: [id]) agent Agent @relation(fields: [agentId], references: [id]) @@index([tenantId, agentId, occurredAt]) @@map("agent_state_events") } // agente.md secao 48. model PauseReason { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid name String code String description String? maxDuration Int? @map("max_duration") paid Boolean @default(false) enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") tenant Tenant @relation(fields: [tenantId], references: [id]) pauseEvents AgentPauseEvent[] @@unique([tenantId, code]) @@index([tenantId]) @@map("pause_reasons") } model AgentPauseEvent { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid agentId String @map("agent_id") @db.Uuid pauseReasonId String @map("pause_reason_id") @db.Uuid startedAt DateTime @default(now()) @map("started_at") endedAt DateTime? @map("ended_at") tenant Tenant @relation(fields: [tenantId], references: [id]) agent Agent @relation(fields: [agentId], references: [id]) pauseReason PauseReason @relation(fields: [pauseReasonId], references: [id]) @@index([tenantId, agentId]) @@map("agent_pause_events") } enum CampaignStatus { DRAFT READY WAITING_SCHEDULE RUNNING PAUSED DRAINING STOPPED COMPLETED ERROR @@map("campaign_status") } // Tabela tenant-scoped protegida por RLS (agente.md secao 63-66). Só o // modelo/CRUD e as transições de status desta fase — o motor que de fato // origina chamadas (PredictiveDialerEngine, secao 72-73) é uma fase à parte. model Campaign { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid name String description String? queueId String @map("queue_id") @db.Uuid trunkId String @map("trunk_id") @db.Uuid callerIdName String? @map("caller_id_name") callerIdNumber String? @map("caller_id_number") timezone String @default("America/Sao_Paulo") startDate DateTime? @map("start_date") @db.Date endDate DateTime? @map("end_date") @db.Date // ISO-8601 (1=segunda ... 7=domingo). daysOfWeek Int[] @map("days_of_week") // "HH:MM", interpretado na timezone da campanha — sem tipo TIME nativo // pra manter o schema simples nesta fase (o scheduler de verdade é da // fase Predictive Engine). startTime String? @map("start_time") endTime String? @map("end_time") maxCps Int? @map("max_cps") maxConcurrentCalls Int? @map("max_concurrent_calls") pacingInitial Float @default(1.0) @map("pacing_initial") pacingMin Float @default(1.0) @map("pacing_min") pacingMax Float @default(3.0) @map("pacing_max") targetAbandonRate Float @default(0.03) @map("target_abandon_rate") ringTimeout Int @default(30) @map("ring_timeout") maxAttempts Int @default(3) @map("max_attempts") recordingEnabled Boolean @default(false) @map("recording_enabled") avmdEnabled Boolean @default(false) @map("avmd_enabled") aiTranscriptionEnabled Boolean @default(false) @map("ai_transcription_enabled") aiAnalysisEnabled Boolean @default(false) @map("ai_analysis_enabled") status CampaignStatus @default(DRAFT) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") tenant Tenant @relation(fields: [tenantId], references: [id]) queue Queue @relation(fields: [queueId], references: [id]) trunk Trunk @relation(fields: [trunkId], references: [id]) leads Lead[] callAttempts CallAttempt[] campaignStats CampaignStats[] @@unique([tenantId, name]) @@index([tenantId]) @@map("campaigns") } enum LeadStatus { NEW READY RESERVED ORIGINATING RINGING ANSWERED QUEUEING CONNECTED_AGENT BUSY NO_ANSWER FAILED VOICEMAIL CALLBACK COMPLETED DO_NOT_CALL MAX_ATTEMPTS @@map("lead_status") } // Tabela tenant-scoped protegida por RLS (agente.md secao 67-68). A reserva // atômica (READY -> RESERVED, `FOR UPDATE SKIP LOCKED`, secao 78) é // implementada na fase CPS Limiter/Predictive Engine — aqui só o modelo e // o CRUD/import. model Lead { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid campaignId String @map("campaign_id") @db.Uuid name String? phoneOriginal String @map("phone_original") 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") customFields Json @default("{}") @map("custom_fields") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") tenant Tenant @relation(fields: [tenantId], references: [id]) campaign Campaign @relation(fields: [campaignId], references: [id]) callAttempts CallAttempt[] @@unique([campaignId, phoneNormalized]) @@index([tenantId]) @@index([campaignId, status, nextAttemptAt]) @@map("leads") } // Lista de bloqueio (agente.md secao 71) — tenant-scoped, checada antes de // qualquer originate (checagem em si é da fase Predictive Engine/CPS // Limiter, aqui só o modelo/CRUD). model SuppressionEntry { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid phoneNormalized String @map("phone_normalized") reason String? createdAt DateTime @default(now()) @map("created_at") tenant Tenant @relation(fields: [tenantId], references: [id]) @@unique([tenantId, phoneNormalized]) @@index([tenantId]) @@map("suppression_entries") } enum CallAttemptStatus { CREATED RESERVED ORIGINATING ORIGINATED RINGING ANSWERED QUEUEING AGENT_CONNECTED COMPLETED BUSY NO_ANSWER FAILED ABANDONED @@map("call_attempt_status") } // Uma tentativa de discagem de um lead (agente.md secao 81-82). Os // identificadores daqui (id, campaignId, leadId, originationUuid) viram // channel variables b2bcall_* no originate (secao 81) — é assim que // b2bcall-fs-events consegue popular NormalizedEvent.tenantId/b2bcallCallId // pra chamadas do discador, diferente das chamadas internas (extension a // extension) que não carregam esses vars ainda. model CallAttempt { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid campaignId String @map("campaign_id") @db.Uuid leadId String @map("lead_id") @db.Uuid agentId String? @map("agent_id") @db.Uuid // UUID do channel no FreeSWITCH (real ou simulado) — null até o originate // de fato rodar (status ainda CREATED/RESERVED). originationUuid String? @unique @map("origination_uuid") @db.Uuid status CallAttemptStatus @default(CREATED) // agente.md secao 185: em modo simulação, nenhuma chamada PSTN real // acontece — answer/busy/no_answer/failed/ringing/delay/talk_time são // sorteados em software, nunca originados de verdade pro trunk. simulated Boolean @default(false) hangupCause String? @map("hangup_cause") talkTimeSeconds Int? @map("talk_time_seconds") createdAt DateTime @default(now()) @map("created_at") ringingAt DateTime? @map("ringing_at") answeredAt DateTime? @map("answered_at") bridgedAt DateTime? @map("bridged_at") endedAt DateTime? @map("ended_at") tenant Tenant @relation(fields: [tenantId], references: [id]) campaign Campaign @relation(fields: [campaignId], references: [id]) lead Lead @relation(fields: [leadId], references: [id]) agent Agent? @relation(fields: [agentId], references: [id]) @@index([tenantId]) @@index([campaignId, status]) @@map("call_attempts") } // Estatísticas EWMA por campanha (agente.md secao 73-76), consultadas e // atualizadas a cada tick do PredictiveDialerEngine — persistidas pra // sobreviver a reinícios do worker (não é cache descartável). Valores // iniciais são estimativas conservadoras (secao 74: "não precisa Machine // Learning, preferir algoritmo estatístico determinístico e explicável"), // convergem conforme tentativas reais completam. model CampaignStats { campaignId String @id @map("campaign_id") @db.Uuid tenantId String @map("tenant_id") @db.Uuid answerProbability Float @default(0.4) @map("answer_probability") averageAnswerDelay Float @default(5) @map("average_answer_delay") averageTalkTime Float @default(180) @map("average_talk_time") abandonRate Float @default(0) @map("abandon_rate") // Fator de pacing aplicado no cálculo de quantas chamadas originar por // tick (secao 76) — começa em Campaign.pacingInitial, ajustado pelo // controle de abandono (secao 84), sempre dentro de [pacingMin, pacingMax]. pacingFactor Float @default(1.0) @map("pacing_factor") updatedAt DateTime @updatedAt @map("updated_at") tenant Tenant @relation(fields: [tenantId], references: [id]) campaign Campaign @relation(fields: [campaignId], references: [id]) @@index([tenantId]) @@map("campaign_stats") }