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.