Distribuição em Campanhas
Fluxo de distribuição de operadores em atendimentos criados por campanhas: pré-atribuição, distribuição automática e quebra de conversa.
Quando um atendimento é criado a partir de uma campanha, o fluxo de distribuição de operadores possui características específicas que diferem do fluxo de atendimentos iniciados por contato direto.
A distribuição passa por duas camadas:
- Pré-atribuição pela campanha (
campaign_pre_assigned): quando a campanha possui uma lista de operadores, um operador elegível é selecionado via round-robin no momento do envio. Esse fluxo é independente da flag global de distribuição automática. - Distribuição automática (
processDistributeHostPeople): quando a campanha não possui lista de operadores (ou quando todos os operadores da lista estão inelegíveis), a distribuição automática do host assume a responsabilidade de atribuir o operador.
Configurações Relevantes
| Configuração | Efeito sobre campanhas |
|---|---|
distributeConversations (flag global) | Controla a distribuição automática. Não afeta o fluxo campaign_pre_assigned quando a campanha possui lista de operadores. |
splitConversations | Habilita a criação de nova conversa quando o contato já possui conversa pré-existente. |
splitIntervalDays | Dias desde a criação da conversa para considerar elegível para split (padrão: 2 dias). |
splitIntervalLastMessageMin | Minutos desde a última mensagem para considerar elegível para split (padrão: 15 min). |
splitTryBestConversations | Se habilitado, tenta reaproveitar uma conversa válida existente antes de criar uma nova. |
Lista de operadores da campanha (hostPeopleIds) | Quando presente, ativa o fluxo campaign_pre_assigned com round-robin entre os elegíveis. |
pauseConversation | Controla se a IA responde quando o operador não está disponível. |
Fluxo de Distribuição
Elegibilidade de Operadores
Antes de pré-atribuir qualquer operador via lista da campanha, um filtro de elegibilidade é aplicado (filterEligibleOperators). Operadores inativos (active: false) são removidos da lista elegível.
- Se operadores elegíveis sobram → um é selecionado via round-robin e atribuído à notificação (
host_people_idna notification). - Se nenhum operador elegível sobra → as notificações são criadas com
host_people_id = nulle o fluxo de distribuição automática (processDistributeHostPeople) assume a responsabilidade pela atribuição.
O campo host_people_id nunca é explicitamente sobrescrito com null via update no Supabase quando não há operador elegível — o campo é removido do payload de update (removeNullFields), preservando qualquer valor residual de atribuições anteriores. Para garantir limpeza, uma nova conversa (via split) sempre é criada com host_people_id = null.
Pré-atribuição via Campanha
Ativado quando a campanha possui lista de operadores e ao menos um operador elegível existe.
Comportamento:
- Seleção por round-robin entre operadores elegíveis.
- Atribuição ocorre no momento do envio da campanha, antes da conversa ser aberta pelo contato.
- Independente da flag global
distributeConversations. - Logs no Typesense registram:
distribution_origin: campaign_pre_assigneddistribution_result: pre_assignedselection_source: campaign_round_robinselection_reason: "operador pré-atribuído pela campanha via round-robin"
Distribuição Automática
Ativado quando:
- A campanha não possui lista de operadores, ou
- Todos os operadores da lista estão inativos (nenhum elegível).
Comportamento:
- Execução fire-and-forget (sem await), sem fila RabbitMQ.
- Se não há operadores ativos no host: registra
distribution_result: 'no_operators_available'+Log.warning. Não gera critical log nem retry. - Se a flag
distributeConversationsforfalse: registradistribution_result: 'not_assigned'sem critical log.
Logs gerados pelo fluxo de distribuição automática não carregam contexto de campanha (from_campaign: false, campaign_id: null), mesmo que o atendimento tenha sido originado por campanha. Esse é o comportamento esperado.
Quebra de Conversa (Split)
Quando o contato já possui uma conversa pré-existente, o envio de campanha aciona o processo de split antes de distribuir o operador.
Fluxo:
1. Busca a conversa mais recente do lead pelo hostId.
Se houver conversa mais nova que a passada, usa ela como referência.
2. Guard: hostConfig.splitConversations
└─ false → sem split, retorna a conversa original.
3. Avaliação de elegibilidade para split:
├─ Conversa fechada → split imediato
└─ Conversa aberta → split somente se AMBAS as condições:
a) Criada há mais de splitIntervalDays dias
b) Última mensagem há mais de splitIntervalLastMessageMin minutos
4. Conversa arquivada + NÃO deve fazer split → desarquiva e retoma.
Conversa arquivada + DEVE fazer split → mantém arquivada, cria nova.
5. Se split necessário:
├─ splitTryBestConversations=true → tenta reaproveitar conversa válida.
├─ Fecha todas as outras conversas abertas do lead.
├─ Migra followups ativos da conversa antiga para a nova.
├─ Limpa cache de latestConversation do lead.
└─ Nova conversa criada com host_people_id = null.
6. Após split (ou sem split):
├─ Com operador pré-atribuído: aplica via addContextToConversation.
└─ Sem operador pré-atribuído: distribuição automática assume.
Garantia de não herança: a nova conversa é criada com host_people_id = null. Operadores residuais de conversas anteriores não são propagados. A atribuição parte sempre do zero.
Cenários Mapeados
Sem lista de operadores + distribuição automática ativada
Todos os operadores desativados
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Não informada |
distributeConversations | true |
| Estado dos operadores | Todos active: false |
Resultado:
- Nenhum operador distribuído.
processDistributeHostPeopleé chamado, encontra apenas operadores inativos.- Logs:
distribution_result: 'no_operators_available',selection_source: automatic_distribution.
Todos os operadores ativados
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Não informada |
distributeConversations | true |
| Estado dos operadores | Todos active: true |
Resultado:
- Operadores distribuídos automaticamente via
processDistributeHostPeople. - Round-robin entre todos os operadores ativos do host.
Com lista de operadores + distribuição automática ativada
Todos os operadores da lista desativados
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Informada |
distributeConversations | true |
| Estado dos operadores na lista | Todos active: false |
Resultado:
filterEligibleOperatorsremove todos (todos inativos).- Notificações criadas com
host_people_id = null. processDistributeHostPeopleassume como fallback.- Se operadores ativos existem fora da lista → distribuídos pelo fluxo automático.
- Se nenhum operador ativo no host →
distribution_result: 'no_operators_available'.
Logs do fluxo automático não carregam campaign_id neste cenário de fallback.
Todos os operadores da lista ativados
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Informada (ex: 6 operadores) |
distributeConversations | true |
| Estado dos operadores na lista | Todos active: true |
Resultado:
- Todos os operadores elegíveis pré-atribuídos via round-robin.
- 1 operador por conversa, distribuição balanceada.
campaign_without_operator: 0- Logs:
distribution_origin: campaign_pre_assigned,distribution_result: pre_assigned,selection_source: campaign_round_robin.
Interação com configurações globais de distribuição
distributeConversations=false + campanha COM lista + operadores ativos
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Informada |
distributeConversations | false |
| Estado dos operadores na lista | Todos active: true |
Resultado:
- Fluxo
campaign_pre_assignedfunciona normalmente — independente da flag global. - 1 operador por conversa via round-robin.
- Logs:
distribution_origin: campaign_pre_assigned,distribution_result: pre_assigned.
A flag distributeConversations=false só bloqueia a distribuição automática (processDistributeHostPeople). O fluxo de pré-atribuição por lista de campanha não é afetado.
distributeConversations=false + campanha SEM lista + operadores ativos
| Configuração | Valor |
|---|---|
| Lista de operadores da campanha | Não informada |
distributeConversations | false |
| Estado dos operadores | Todos active: true |
Resultado:
- Sem pré-atribuição (campanha não possui lista).
processDistributeHostPeopleé chamado, mas barrado pela flag.- Nenhum operador distribuído.
- Logs:
distribution_result: 'not_assigned',distribute_conversations_enabled: false.
Cenários Ainda Não Mapeados
Os cenários abaixo foram identificados como relevantes, mas ainda não foram testados ou validados.
Estado misto de operadores
| Cenário | Descrição |
|---|---|
| Operadores da lista ativados + fora da lista desativados | Verifica se round-robin respeita escopo da lista |
| Operadores da lista desativados + fora da lista ativados | Valida fallback para distribuição automática |
| Mix ativo/inativo dentro da lista | Round-robin entre elegíveis apenas |
| Apenas 1 operador na lista (ativo ou inativo) | Caso limite do round-robin |
Mudança de estado durante ciclo de vida da campanha
| Cenário | Descrição |
|---|---|
| Operador ativo na criação, desativado antes do envio | Filtro de elegibilidade reavalia no envio? |
| Operador inativo na criação, ativado antes do envio | Entra na lista de elegíveis? |
| Operador removido entre criação e envio | Tratamento de referência órfã |
Contato com conversa pré-existente
| Cenário | Descrição |
|---|---|
| Conversa aberta com operador atribuído | Split respeita/ignora operador anterior? |
| Conversa fechada com operador anterior | Nova conversa herda operador? |
| Conversa com operador agora inativo | Comportamento do filtro pós-split |
Opção “Eu mesmo” (Myself)
| Cenário | Descrição |
|---|---|
| Apenas “Eu mesmo” como operador | Tratamento do myselfAsOperator no backend |
| “Eu mesmo” + outros operadores | Round-robin inclui o criador |
| “Eu mesmo” inativo | Elegibilidade do criador da campanha |
O frontend possui tratamento especial para myselfAsOperator, adicionando o ID do usuário logado à lista de hostPeopleIds. O comportamento do filtro de elegibilidade nesses casos não foi validado.
Flag pauseConversation
| Cenário | Descrição |
|---|---|
pauseConversation=true + operadores ativos | IA pausada, operador assume |
pauseConversation=true + todos inativos | IA pausada sem operador disponível |
pauseConversation=false | IA continua respondendo |
Campanhas concorrentes
| Cenário | Descrição |
|---|---|
| Duas campanhas simultâneas com mesmos operadores | Round-robin se mantém justo? |
| Duas campanhas para o mesmo contato com operadores diferentes | Conflito de atribuição |
Sincronização ticket-conversa
| Cenário | Descrição |
|---|---|
| Sincronização habilitada | Operador do ticket acompanha o da conversa |
| Sincronização desabilitada | Operador do ticket pode divergir |
Comportamentos de Log
Campos relevantes no Typesense (tolky_features_logs)
| Campo | Valores possíveis | Significado |
|---|---|---|
distribution_origin | campaign_pre_assigned, automatic | Origem da distribuição |
distribution_result | pre_assigned, not_assigned, no_operators_available | Resultado da tentativa |
selection_source | campaign_round_robin, automatic_distribution | Mecanismo de seleção |
selection_reason | texto descritivo | Justificativa da decisão |
from_campaign | true / false | Se o log veio de envio de campanha |
campaign_id | UUID ou null | ID da campanha de origem |
assigned_operator_id | UUID ou null | Operador atribuído |
available_operators_count | número inteiro | Total de operadores elegíveis |
distribute_conversations_enabled | true / false | Estado da flag global no momento |
Contexto de campanha nos logs
Logs gerados pelo fluxo processDistributeHostPeople (distribuição automática) não carregam campaign_id nem from_campaign: true, mesmo que o atendimento tenha sido originado por uma campanha.
Logs com contexto de campanha (from_campaign: true) só aparecem quando o fluxo campaign_pre_assigned é ativado (campanha com lista de operadores elegíveis).