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:

  1. 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.
  2. 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çãoEfeito 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.
splitConversationsHabilita a criação de nova conversa quando o contato já possui conversa pré-existente.
splitIntervalDaysDias desde a criação da conversa para considerar elegível para split (padrão: 2 dias).
splitIntervalLastMessageMinMinutos desde a última mensagem para considerar elegível para split (padrão: 15 min).
splitTryBestConversationsSe 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.
pauseConversationControla 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_id na notification).
  • Se nenhum operador elegível sobra → as notificações são criadas com host_people_id = null e 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_assigned
    • distribution_result: pre_assigned
    • selection_source: campaign_round_robin
    • selection_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 distributeConversations for false: registra distribution_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çãoValor
Lista de operadores da campanhaNão informada
distributeConversationstrue
Estado dos operadoresTodos 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çãoValor
Lista de operadores da campanhaNão informada
distributeConversationstrue
Estado dos operadoresTodos 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çãoValor
Lista de operadores da campanhaInformada
distributeConversationstrue
Estado dos operadores na listaTodos active: false

Resultado:

  • filterEligibleOperators remove todos (todos inativos).
  • Notificações criadas com host_people_id = null.
  • processDistributeHostPeople assume 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çãoValor
Lista de operadores da campanhaInformada (ex: 6 operadores)
distributeConversationstrue
Estado dos operadores na listaTodos 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çãoValor
Lista de operadores da campanhaInformada
distributeConversationsfalse
Estado dos operadores na listaTodos active: true

Resultado:

  • Fluxo campaign_pre_assigned funciona 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çãoValor
Lista de operadores da campanhaNão informada
distributeConversationsfalse
Estado dos operadoresTodos 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árioDescrição
Operadores da lista ativados + fora da lista desativadosVerifica se round-robin respeita escopo da lista
Operadores da lista desativados + fora da lista ativadosValida fallback para distribuição automática
Mix ativo/inativo dentro da listaRound-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árioDescrição
Operador ativo na criação, desativado antes do envioFiltro de elegibilidade reavalia no envio?
Operador inativo na criação, ativado antes do envioEntra na lista de elegíveis?
Operador removido entre criação e envioTratamento de referência órfã

Contato com conversa pré-existente

CenárioDescrição
Conversa aberta com operador atribuídoSplit respeita/ignora operador anterior?
Conversa fechada com operador anteriorNova conversa herda operador?
Conversa com operador agora inativoComportamento do filtro pós-split

Opção “Eu mesmo” (Myself)

CenárioDescrição
Apenas “Eu mesmo” como operadorTratamento do myselfAsOperator no backend
“Eu mesmo” + outros operadoresRound-robin inclui o criador
“Eu mesmo” inativoElegibilidade 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árioDescrição
pauseConversation=true + operadores ativosIA pausada, operador assume
pauseConversation=true + todos inativosIA pausada sem operador disponível
pauseConversation=falseIA continua respondendo

Campanhas concorrentes

CenárioDescrição
Duas campanhas simultâneas com mesmos operadoresRound-robin se mantém justo?
Duas campanhas para o mesmo contato com operadores diferentesConflito de atribuição

Sincronização ticket-conversa

CenárioDescrição
Sincronização habilitadaOperador do ticket acompanha o da conversa
Sincronização desabilitadaOperador do ticket pode divergir

Comportamentos de Log

Campos relevantes no Typesense (tolky_features_logs)

CampoValores possíveisSignificado
distribution_origincampaign_pre_assigned, automaticOrigem da distribuição
distribution_resultpre_assigned, not_assigned, no_operators_availableResultado da tentativa
selection_sourcecampaign_round_robin, automatic_distributionMecanismo de seleção
selection_reasontexto descritivoJustificativa da decisão
from_campaigntrue / falseSe o log veio de envio de campanha
campaign_idUUID ou nullID da campanha de origem
assigned_operator_idUUID ou nullOperador atribuído
available_operators_countnúmero inteiroTotal de operadores elegíveis
distribute_conversations_enabledtrue / falseEstado 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).