Atendro: ligações simultâneas

Prompt de implementação: limite versionado por webhook, vagas e reservas entre atendentes.

Este texto pode ser enviado ao agente que trabalha no repositório atendro-ai. O contrato abaixo foi preparado no AtendroZAP, mas ainda não foi publicado na VPS 1. A implementação no consumidor pode avançar com fixtures; o teste real depende de capabilities.calls_settings_webhook=true no servidor escolhido. Nenhuma mudança de instâncias, webhooks de produção ou limite em produção é autorizada por este documento.

Texto para implementar no atendro-ai

Implemente a sincronização do limite de ligações simultâneas por instância AtendroZAP e a gestão das vagas no backend e na interface do Atendro. Inspecione primeiro os módulos atuais de provedor, conexões, webhooks, chamadas, ponte de mídia e permissões. Reutilize os mecanismos de inbox, assinatura e reconexão já existentes; preserve os demais provedores e os contratos em uso.

O usuário precisa conseguir configurar uma instância para duas ligações, ver o limite atualizado quando ele mudar no painel do AtendroZAP e distribuir as duas vagas entre atendentes. Voz e vídeo compartilham o limite. Isso é uma configuração de admissão do provedor: mais de uma ligação simultânea no mesmo número ainda não foi homologada em aparelhos reais. Não apresente o número configurado como capacidade física comprovada ou como duas linhas WhatsApp independentes.

Contrato do provedor

Use a URL do servidor vinculado à conexão, nunca uma VPS fixa no código. Credenciais ficam no backend. As rotas de instância recebem header token; rotas administrativas usam admintoken, acessível somente a administradores autorizados. Não exponha esses segredos em bundle, navegador, URL, log ou erro.

  • GET /v1/capabilities anuncia capabilities.calls, capabilities.calls_per_instance_limit e capabilities.calls_settings_webhook; calls_max_concurrent_range contém {min: 1, max: 8}. calls_max_concurrent é o padrão do servidor, não o limite salvo da instância. Para vídeo, exija também capabilities.calls_video.
  • GET /instance/status retorna instance.kind, instance.callsMaxConcurrent e instance.callsConfigVersion para kind: calls. A versão é um inteiro de 64 bits, inclusive 0; não confunda zero com ausência. O campo de data HTTP existente é instance.updated, não updatedAt.
  • POST /instance/call-settings, com header token, recebe {"callsMaxConcurrent": 2}. A variante administrativa é POST /v1/instances/{id}/call-settings. A resposta contém o objeto instance com limite e versão canônicos. Cada POST aceito incrementa a versão, mesmo repetindo o mesmo valor. Não existe comparação condicional de versão nesta rota: escritas concorrentes vencem pela última versão salva.
  • O limite de 1 a 8 conta chamadas recebidas e realizadas, de voz ou vídeo, em ringing, connecting e active, além das discagens que o serviço já reservou. Uma chamada de vídeo ocupa uma vaga, mesmo usando dois sockets.
  • Reduzir o limite preserva chamadas vivas. Se houver duas e o limite cair para um, nenhuma é encerrada; novas chamadas só entram quando o total ficar abaixo de um. Sem vaga, uma discagem recebe 409 line_busy. A API não cria uma fila de discagem automática; não repita chamadas em loop ao liberar vaga.
  • A alteração pode ser salva desconectada. O worker local aplica sem reconectar; outro worker aplica na próxima renovação de lease e antes de cada nova discagem de saída. Uma confirmação de configuração não comprova que uma ligação de teste foi realizada.
  • GET /call/active retorna {calls: [...]} para a instância. A lista não mostra reservas de discagem que ainda não receberam callId. GET /call/{id} e GET /call/history permitem reconciliar chamadas e fins.

Inscrição e processamento dos webhooks

No receptor HMAC existente, acrescente call_settings à lista de eventos reconhecidos. Uma instância calls deve assinar call, call_settings e connection; limits é opcional conforme a integração. Ela não publica mensagens, recibos, edições, grupos ou undecryptable. call_settings não é um subevento call e não contém callId: nunca crie uma chamada a partir dele.

Ao preparar a inscrição, consulte GET /webhook e mescle os nomes necessários com os eventos já salvos. Preserve URL, segredo, filtros, estado habilitado e demais opções. Para a mesma URL, omitir secret preserva o segredo existente; não envie segredo vazio ou inventado. O GET não devolve o segredo. Se o cadastro for herdado do global, preserve a herança: alteração global exige permissão administrativa e avaliação das demais instâncias. Não transforme silenciosamente um webhook herdado em exceção própria nem substitua configurações de clientes. Se a inscrição estiver desabilitada ou ausente, siga o fluxo autorizado de provisionamento do produto, sem ativá-la como efeito colateral de mudar o limite.

O AtendroZAP grava a configuração e o evento atomicamente. Inscrição ausente não impede salvar o limite, mas não há entrega sem assinar call_settings. Registrar o webhook não produz snapshot inicial. Depois de preparar a inscrição, leia o status para obter o estado atual e resolver a corrida com eventos recebidos durante essa leitura.

Exemplo sintético parcial do novo evento, no envelope assinado atual:

json
{
  "EventType": "call_settings",
  "event": "call_settings",
  "eventId": "00000000-0000-4000-8000-000000000002",
  "instanceId": "00000000-0000-4000-8000-000000000001",
  "seq": 42,
  "data": {
    "kind": "calls",
    "callsMaxConcurrent": 2,
    "callsConfigVersion": 3,
    "updatedAt": "2026-09-19T15:00:00.123456789Z"
  }
}

updatedAt é UTC em RFC3339Nano. O evento connection mantém seu formato e acrescenta instance.kind: calls; quando a leitura do banco é possível, também traz instance.callsMaxConcurrent e instance.callsConfigVersion. Esses campos opcionais em connection não ficam em data. Se ausentes, preserve o limite conhecido e reconcilie pelo status.

Valide a assinatura HMAC-SHA256 sobre timestamp + "." + corpo bruto, a janela de timestamp e o vínculo servidor/instância/empresa antes de aceitar. Deduplique por identidade do servidor, instanceId e eventId; confirme 2xx somente após persistência durável. Retries preservam o eventId. Use seq do envelope para ordenar eventos; a versão de configuração é um contador separado.

Persista limite e versão juntos. Aplique snapshots de resposta POST, GET, call_settings e connection pela mesma regra transacional: versão maior substitui, menor é ignorada, igual é idempotente. Uma mesma versão com valor divergente exige reconciliação, sem escolher pelo horário. Campos ausentes em versões antigas da API não equivalem a zero. Não use updatedAt, horário de chegada ou ordem das promessas HTTP para sobrescrever uma versão maior. Use representação que preserve inteiros de 64 bits nos componentes que precisarem deles; não faça comparação textual lexicográfica de versões.

Após um POST, aplique a resposta canônica e reconcilie com GET. Se houver timeout ou perda de resposta, leia o status antes de repetir a alteração. Notifique as sessões do Atendro vinculadas à conexão após o commit, usando o canal de atualização que o produto já possui. Uma mudança feita fora do Atendro deve aparecer sem exigir novo pareamento ou recarregar a página.

Gestão de vagas e operação pelos atendentes

Mantenha a identidade da chamada como (servidor, instanceId, callId); no escopo de um servidor, (instanceId, callId). Um mesmo callId pode existir em duas instâncias quando ambas as pontas estão pareadas. Conte recebidas e realizadas, incluindo as que estão tocando. answered confirma atendimento em connecting; não espere active para reservar a vaga.

Eventos video atualizam a mesma chamada e não consomem vaga adicional. Eventos repetidos ou fora de ordem não podem incrementar o contador de novo. ended libera uma vez e conserva o primeiro motivo/origem de encerramento; eventos antigos não reabrem a chamada. recording_ready e recording_failed só atualizam a gravação: não ocupam nem liberam uma vaga por conta própria. Evite um contador isolado de incrementos/decrementos; derive a ocupação de registros idempotentes de chamadas e reservas válidas.

Antes de discar, faça reserva atômica da vaga no backend compartilhado do Atendro. Duas abas, atendentes ou réplicas podem discar ao mesmo tempo; desabilitar o botão no navegador não impede essa disputa. Conte reservas pendentes ainda sem callId, atribua identidade à tentativa e reutilize sua Idempotency-Key ao repetir a mesma discagem. Ao receber callId, converta a reserva em chamada sem contar duas vezes. Correlacione também quando o webhook chegar antes da resposta HTTP. Em falha definitiva ou line_busy, libere a reserva e reconcilie. Em resultado ambíguo, preserve-a de forma conservadora até resolver pelo mecanismo de idempotência e consulta do provedor; o GET de chamadas ativas não mostra reservas internas ainda sem ID. Não libere uma reserva apenas porque o navegador fechou ou uma resposta demorou.

O provedor continua sendo a autoridade final de admissão: ligações recebidas podem ocupar uma vaga entre a checagem local e a discagem. Trate 409 line_busy como condição operacional, atualize a ocupação e informe o atendente. Quando sem vaga, mostre o limite e as chamadas que a ocupam. Se o Atendro implementar espera, ela deve ser explícita e cancelável; não iniciar uma ligação futura sem a intenção do atendente ainda válida.

Mostre, por conexão, limite configurado, chamadas vivas e tentativas pendentes. Disponibilidade é no mínimo zero; ao reduzir abaixo da ocupação, mostre a situação excedente sem desligar ninguém. A permissão de alterar o limite deve ser validada no backend, respeitar empresa e conexão e aceitar somente inteiros de 1 a 8. Estado desconectado bloqueia discagem, mas permite configurar o limite.

Cada chamada precisa de proprietário/atendente e ponte de mídia próprios, identificados por instância e callId. Não use um único socket ou estado global por número/conexão. Dois atendentes não devem tomar a mesma chamada por acidente; o AtendroZAP aceita substituir um attachment, o que não é um bloqueio de propriedade de atendimento. Preserve as regras existentes de transferência. Microfone, reprodução e câmera devem ser associados à chamada atendida e ao usuário correto, sem misturar áudio entre chamadas.

Mantenha a janela existente de reconexão da ponte: o AtendroZAP tolera 30 s sem socket de áudio/vídeo e o Atendro retém a sessão por 25 s. Desligamento voluntário usa /call/hangup; câmera desligada usa /call/video com action: stop e não libera a vaga. Uma queda de socket não é, sozinha, prova de ended.

Ao abrir/reconectar a tela, ao reconectar o receptor e após erros de admissão, reconcilie configuração e chamadas com GET /instance/status e GET /call/active. Consulte o estado/histórico das chamadas locais que desapareceram da lista para obter o encerramento real. Proteja essa leitura contra eventos mais novos recebidos em paralelo; preserve as reservas ainda em resolução. Faça reconciliação periódica moderada como recuperação de eventos perdidos, sem depender de polling rápido para a operação normal.

Testes e entrega

Cubra com testes significativos, usando mocks/fixtures e banco descartável:

  1. Limite alterado no AtendroZAP de 1 para 2 chega via webhook e atualiza persistência, interface e admissão sem reconectar a instância.
  2. Configuração via Atendro usa credencial no backend, valida permissões, aceita 1 a 8 e rejeita valores inválidos; falha remota não aparenta sucesso.
  3. Snapshot inicial, versão zero, HTTP antigo após webhook novo, eventos duplicados/fora de ordem, assinatura inválida e instância de outra empresa.
  4. Duas reservas concorrentes com limite 1 admitem apenas uma; com limite 2 admitem duas e recusam a terceira, inclusive entre réplicas do backend.
  5. Webhook chega antes da resposta de discagem; timeout ambíguo não duplica chamada nem libera a vaga prematuramente; 409 line_busy reconcilia.
  6. Chamada recebida disputa vaga com saída; vídeo e gravação não contam duas vezes; duas instâncias com o mesmo callId continuam separadas.
  7. Redução de 2 para 1 com duas chamadas vivas preserva ambas e bloqueia novas.
  8. Reload, queda do receptor, reconexão dos sockets e atualização concorrente preservam propriedade, contagem e primeiro encerramento.
  9. Inscrição mescla eventos sem perder URL, segredo, filtros ou herança; instância de mensagens e demais provedores conservam o comportamento atual.

Entregue mudanças, migrações necessárias, testes executados e instruções de ativação. Não publique nem altere instâncias, limites, webhooks ou credenciais de produção apenas para executar testes. Separe o resultado dos testes locais da homologação pendente de duas chamadas WhatsApp reais no mesmo número.

Referências: Chamadas, Eventos, Receptor HMAC e OpenAPI.