Sincronização de unificação e troca de e-mail com o checkout novo
TLDR: toda unificação de cadastros e toda troca de e-mail de cliente passa a gerar um
OutboxEventpara oibft-backend, reaproveitando o outbox que já existe — sem endpoint novo deste lado, sem chamada HTTP no fluxo do operador.
Contexto
No checkout-api o cliente é um cadastro por e-mail, e é ele que se liga a várias empresas — os pagamentos apontam direto para o cadastro, não para o par cliente-empresa. Quando o suporte encontra a mesma pessoa em dois cadastros, roda a unificação: pagamentos e vínculos com empresa do cadastro errado passam para o correto, o errado é desativado e o e-mail dele é liberado.
Isso já é propagado para os sistemas a jusante (CBTRG e Onion por webhook, Asaas pela API do gateway, Apolo por REST), tudo a partir de IbftEmailSyncService. Falta o ibft-backend: os pedidos importados pelo contrato de reparcelamento ficam pendurados no cadastro que saiu, e o financeiro passa a ver a dívida partida entre dois compradores que são a mesma pessoa. Pior: o e-mail que sai deixa de existir aqui, então se ele continuar ativo lá, a próxima sincronização de reparcelamento casa pelo e-mail antigo e recria a divisão.
O contrato do endpoint receptor (POST /v1/sync/user no ibft-backend) já foi acordado — payload {email, new_email}, respostas merged/updated/noop, mesmo token Bearer da sincronização de reparcelamento. Este spec cobre só o lado que dispara.
Os dois gatilhos já convergem num único ponto: a troca de e-mail (callback after_update em Customer) e a unificação (IbftCustomerMergeService) ambas enfileiram IbftEmailSyncJob, que chama IbftEmailSyncService#sync. Basta um destino novo ali.
Objetivos
- Adicionar um destino em
IbftEmailSyncService#syncque grava umOutboxEvent(event_name: "new_checkout.customer_sync") com o payload{email, new_email}que o serviço já monta hoje. - Reaproveitar integralmente o outbox existente: mesma tabela, mesmo job de despacho, mesmas 3 tentativas, mesma tela de admin — só um despachante novo registrado em
Outbox::DispatchRegistry. - Despachar para
POST #{NEW_CHECKOUT_API_URL}/v1/sync/user, com o mesmoAuthorization: Bearer #{NEW_CHECKOUT_API_TOKEN}e o mesmo timeout do despachante de reparcelamento. - Disparar em toda unificação e toda troca de e-mail, sem filtrar por organização.
- Documentar o contrato da Funcionalidade 2 em
.project/docs/reference/payments/, que hoje só cobre o reparcelamento.
Fora de escopo
- Filtro por organização (CITRG). Diferente do reparcelamento — onde o disparo é restrito à CITRG porque o pagamento pertence a um produto de uma empresa — a unificação é global por natureza: o mesmo cadastro pode ter pedidos em mais de uma empresa e todos mudam de dono na mesma operação. Filtrar aqui arrisca perder uma unificação em silêncio (por exemplo, quando o vínculo com a CITRG está no cadastro que está sendo desativado), que é exatamente o bug que esta feature existe para evitar. O contrato prevê a resposta
noopjustamente para absorver os casos sem correspondência do outro lado. WebhookLogno enfileiramento. Os outros quatro destinos gravam umWebhookLogpor sincronização; aqui oOutboxEventjá é o registro — com payload, status, tentativas, último erro e tela no admin. UmWebhookLogno enfileiramento registraria “enfileirei”, não “entreguei”.- Implementar o endpoint receptor (
POST /v1/sync/user) — é responsabilidade doibft-backend. - Mudar a lógica de unificação ou de troca de e-mail (
IbftCustomerMergeService, callback doCustomer) — o disparo entra no ponto que já existe, sem alterar o que já é propagado para CBTRG, Asaas, Apolo e Onion. - Caminho manual pelo console (
run_other_services: false) continua sem propagar nada, inclusive para oibft-backend— quem roda assim assume o sincronismo.
Mudanças
app/services/outbox/dispatchers/new_checkout_customer_sync.rb (novo)
- Mesmo formato do
NewCheckoutRepaymentSync:.call(payload)faz oPOSTviaHTTPartypara"#{ENV["NEW_CHECKOUT_API_URL"]}/v1/sync/user", comContent-Type: application/json,Authorization: Bearer #{ENV["NEW_CHECKOUT_API_TOKEN"]}e timeout de 3s. - Sem as envs configuradas, levanta erro claro — o job registra isso como falha da tentativa, como já faz para o reparcelamento.
app/services/outbox/dispatch_registry.rb
- Uma entrada nova:
"new_checkout.customer_sync" => Outbox::Dispatchers::NewCheckoutCustomerSync.
app/services/ibft_email_sync_service.rb
- Novo método
sync_new_checkout, chamado de#syncjunto com os outros quatro destinos:OutboxEvent.create!(event_name: "new_checkout.customer_sync", payload: @payload). @payload({email:, new_email:}) já é montado noinitializee é exatamente o corpo que o contrato espera — nenhuma transformação nova.- Sem condicional: diferente de
sync_onion(que só roda quando o cliente tem pagamento com aquela integração), este destino recebe toda unificação e toda troca de e-mail.
Sobre a transação: diferente do reparcelamento — onde o OutboxEvent nasce dentro da mesma transação que persiste o pagamento — aqui o evento é criado dentro do job, já depois do commit. Isso não enfraquece a garantia: o IbftEmailSyncJob é enfileirado dentro da transação da unificação e, como a fila é o próprio banco (GoodJob), uma transação revertida não deixa job nem evento. Uma falha dentro do job segue o mesmo comportamento que já vale hoje para os outros quatro destinos — este não é tratado de forma diferente.
Specs
spec/services/outbox/dispatchers/new_checkout_customer_sync_spec.rb(novo): URL, header de autenticação, timeout, retorno da resposta e erro quando as envs faltam.spec/services/outbox/dispatch_registry_spec.rb: resolve o novoevent_namepara o despachante certo.spec/services/ibft_email_sync_service_spec.rb(novo):#synccria exatamente umOutboxEventcom oevent_namee o payload corretos; cobre os dois gatilhos (unificação e troca de e-mail) chegando pelo mesmo caminho; confirma que nenhuma chamada HTTP sai no momento do enfileiramento.
Como verificar
bundle exec rspec spec/services/outbox spec/services/ibft_email_sync_service_spec.rb— todos os exemplos passam.bundle exec rspec— suíte completa sem regressão (os outros quatro destinos continuam sendo chamados como antes).bundle exec rubocop app spec .project— 0 ofensas.- Manualmente: alterar o e-mail de um
Customerno admin e confirmar queoutbox_eventsrecebe uma linhapendingcomevent_name: "new_checkout.customer_sync"e payload{email, new_email}— sem nenhuma chamada HTTP no caminho da requisição. - Manualmente: rodar a ação “Unificar” no admin de clientes e confirmar o mesmo, com
email= cadastro que sai enew_email= cadastro que fica. - Com
NEW_CHECKOUT_API_URLapontando para um servidor local, confirmar oPOSTem/v1/sync/userfeito pelo job enfileirado com o evento, e a transiçãopending → sent.
Documentação
- Novo
.project/docs/reference/payments/new_checkout_customer_sync_endpoint.mdcom o contrato da Funcionalidade 2 (gatilhos, payload, respostasmerged/updated/noop, autenticação e política de retry), seguindo o mesmo formato do contrato de reparcelamento. Hoje esse contrato não existe em nenhuma branch mergeada — só no material que originou esta spec. - Indexar o novo doc em
.project/docs/README.md. - Nenhuma regra nova em
.project/docs/rules/: a semântica de despacho (3 tentativas, sem distinção de erro, reenvio manual) é a mesma já coberta por R-004 — este spec só adiciona um consumidor do mesmo mecanismo.