Pular para conteúdo

eGestor: cadastro de contatos

Escopo atual e migração futura

O adapter atual busca, cria e atualiza contatos de clientes. Não emite pedido, cobrança, nota fiscal nem orçamento. O contrato real no código usa /contatos e busca paginada por filtro; não usa /clientes, embora comentários antigos do stub mencionem esse endpoint.

docs/requisitos-tecnicos-api-compativel-egestor.md especifica uma futura API própria compatível. Esse documento é um contrato para desenvolvimento, não uma implementação de servidor presente no repositório. A existência de EGESTOR_BASE_URL configurável permite uma futura substituição; não prova que ela foi implantada ou homologada.

Arquivos, Protocol, factory e stub

Arquivo Papel
src/core/integrations/egestor/interface.py EGestorClient.upsert_customer(payload) -> StubResponse
src/core/integrations/egestor/client.py EGestorHttpConfig, EGestorHttpClient, busca segura e HTTP
src/core/integrations/egestor/payloads.py build_customer_payload()
src/core/integrations/egestor/stub.py EGestorClientStub, sucesso com mock_egestor_cliente_789
src/core/integrations/factory.py build_egestor_client() e emissor de métrica
src/core/application/confirm_action.py Consumidor síncrono após confirmação cadastral

EGESTOR_ENABLED=false seleciona stub. Em modo real, URL e token vêm de EGESTOR_BASE_URL/EGESTOR_API_TOKEN ou dos respectivos *_SSM_PARAM, com valor direto prioritário. URL/token ausentes ou SSM vazio impedem construir o client. Timeout default 10 s; retry default 2.

Payload e autenticação

build_customer_payload(cadastral_data=...) exige nome, produz tipo=["cliente"], normaliza documento removendo pontuação e converte email/telefone em listas. Aceita documento nas chaves cpfcnpj, cpfCnpj ou cpf_cnpj. Endereço, cidade, estado e CEP podem existir na sessão, mas não são enviados por esse builder.

{
  "nome": "Pessoa Exemplo",
  "tipo": ["cliente"],
  "cpfcnpj": "00000000000",
  "emails": ["teste@example.invalid"],
  "fones": ["+5500000000000"]
}

Os números são placeholders fictícios, sem validade cadastral. O client aceita documentos normalizados de 11 ou 14 dígitos, sem verificar dígitos verificadores; o fluxo de confirmação em validators.py faz validação mais estrita antes de chamá-lo. Não confunda o teste de tamanho do adapter com validação de CPF/CNPJ.

Requests de contatos usam Bearer, Accept JSON e Content-Type JSON. _obtain_access_token() implementa uma heurística: se o token começa com ey e tem mais de 50 caracteres, tenta a troca abaixo. Para outras strings, usa o token diretamente.

POST <BASE_URL_RESOLVIDA>/../oauth/access_token
Content-Type: application/json
{"grant_type":"personal","personal_token":"<PERSONAL_TOKEN>"}

Para uma base https://erp.example.invalid/api/v1, a resolução de URL produz /api/oauth/access_token. access_token recebido é cacheado na instância. Não há gestão de expiração nem refresh automático após 401. Exceção na troca é logada em debug como egestor_oauth_exchange_skipped e o client tenta usar o token original; essa troca não tem o loop de retry das operações de contatos.

Busca segura antes de escrever

GET <EGESTOR_BASE_URL>/contatos?filtro=<DOCUMENTO_SO_DIGITOS>&page=1
Authorization: Bearer <TOKEN>
Accept: application/json

O adapter percorre até dez páginas e valida o envelope em cada uma. current_page, last_page e total precisam ser inteiros JSON, não strings/bools; página atual deve corresponder à pedida, total e última página precisam permanecer constantes, e o total final precisa corresponder ao número de objetos lidos. Cada candidato exige codigo e cpfcnpj válido no critério de tamanho. A escolha só aceita um código distinto com documento exatamente igual ao buscado.

{
  "current_page": 1,
  "last_page": 1,
  "total": 1,
  "data": [{"codigo":"customer-demo","cpfcnpj":"00000000000","nome":"Pessoa Exemplo"}]
}

Este retorno autoriza atualizar customer-demo apenas se o documento da busca for o mesmo. Não se atualiza “o primeiro contato retornado”. Sem candidatos, o formato canônico abaixo é aceito:

{"current_page":1,"last_page":0,"total":0,"data":[]}

last_page=0 só é aceito nessa combinação vazia na primeira página; envelopes inconsistentes são bloqueados. HTTP 404 na primeira página também é tratado como contato inexistente. Isso difere do CRM, cujo 404 de busca é falha; ao migrar eGestor, garanta que 404 de rota inexistente não seja usado para esconder configuração errada.

Criação, atualização e sucesso

Condição da busca Request seguinte Corpo
Vazia válida ou 404 inicial POST <BASE>/contatos Payload do builder
Exatamente um documento/código PUT <BASE>/contatos/{codigo} Mesmo payload do builder
Ambígua, insegura ou envelope inválido Nenhuma escrita Falha controlada

Resposta fictícia {"codigo":"customer-demo"} vira StubResponse(success=True, mock_id="customer-demo", detail="egestor_create_success"). A extração de ID de criação aceita id, cliente_id, clienteId, codigo, uuid, inclusive alguns envelopes/listas; a busca é propositalmente mais estrita e exige codigo/cpfcnpj. PUT pode usar o código conhecido como fallback.

POST 409 dispara nova busca completa; um contato exato permite PUT. Busca vazia após conflito mantém o 409; busca insegura mantém o bloqueio. Não há idempotency key. Repetições de POST por falha de rede dependem da unicidade por documento no provider e podem exigir reconciliação.

Catálogo de falhas e efeito na sessão

detail Causa e ação
egestor_missing_document Documento ausente ou comprimento inválido; corrigir cadastro; não houve HTTP
egestor_lookup_invalid_response Envelope incompleto, tipos errados, total divergente ou candidato sem código/documento; conferir contrato
egestor_lookup_unsafe Resultados sem documento exato; bloquear escrita e investigar filtro
egestor_lookup_ambiguous Mais de um código com mesmo documento; resolver duplicidade no ERP
egestor_lookup_pagination_limit Mais de dez páginas; conferir filtro/paginação
egestor_http_401 / 403 Credencial/permissão; sem retry HTTP local
egestor_http_429 / 5xx Limite/indisponibilidade; retries esgotados
egestor_invalid_response JSON inválido na resposta de escrita

_request_json() repete rede/timeout/HTTP 429/5xx até 1 + EGESTOR_RETRY_MAX, com esperas min(2**attempt,8) segundos. JSON inválido e demais 4xx retornam imediatamente. O orçamento é por request; uma busca de dez páginas pode fazer muitas chamadas, além do OAuth e da escrita. Não há deadline global do upsert nem tratamento de Retry-After.

No execute_confirm_action(), contato CRM é tentado primeiro; eGestor depois, mesmo quando CRM retornou falha. Resposta eGestor sem sucesso ou sem ID, ou contato CRM sem sucesso/ID, gera HANDOFF_PENDING, motivo api_failure, resumo e payload eGestor persistidos. Não há rollback dos efeitos já aceitos pelos serviços.

Logging, métrica e segurança

Eventos: egestor_customer_lookup_resolved, egestor_customer_lookup_blocked, egestor_customer_synced, egestor_http_failed, egestor_request_failed, egestor_invalid_response. Lookup bloqueado registra motivo, contagem de candidatos/exatos e páginas; não o CPF/CNPJ nem o corpo remoto.

Na Lambda cujo AWS_LAMBDA_FUNCTION_NAME contém -orchestrator-, a factory injeta métrica GoGenetic/eGestor / UnsafeCustomerLookup, dimensionada por função. Erro ao publicar métrica gera egestor_lookup_metric_failed e não libera a escrita bloqueada. O stub registra payload pelo logger compartilhado; mantenha dados fictícios. O token e os cadastros exigem controle de acesso fora dos logs.

Testes, smoke e substituição

tests/unit/integrations/test_egestor_http_client.py cobre vazio canônico, documento exato fora da primeira posição, divergência, ambiguidade, paginação/limite, 409, retry e logs. tests/unit/integrations/test_factory.py cobre SSM e métrica; tests/unit/application/test_confirm_action.py cobre efeito no cadastro/handoff.

python -m pytest tests/unit/integrations/test_egestor_http_client.py -q

src/core/application/smokes/egestor_smoke.py constrói o client e valida configuração. Sem SMOKE_ALLOW_REAL_WRITE, retorna skipped, não comprova conectividade e não pesquisa contato. Com permissão, chama upsert_customer com cadastro de teste fixo; portanto pode atualizar um contato já existente com aquele documento. O smoke não faz cleanup automático. Veja smokes.

Para futura API própria, preserve filtros, paginação, CPF exato, códigos estáveis, Bearer e semântica 409; configure base terminando no prefixo correto (/api/v1 quando aplicável). Se não houver compatibilidade HTTP, implemente EGestorClient e altere a factory. Mantenha todos os testes de busca segura antes de habilitar escrita. O documento de requisitos futuro é apoio de contrato, sem substituir os testes do adapter vigente.