Status do lado checkout-api: implementado, de forma assíncrona (
IbftEmailSyncService#sync_new_checkoutgrava umOutboxEvent;Outbox::DispatchEventsJob, enfileirado junto com o evento, faz o POST — ver regra de despacho do outbox). Aguardando oibft-backendexpor o endpoint em staging para validação ponta a ponta.
Endpoint de unificação de cliente — ibft-backend
TLDR: o
checkout-apimanda dois e-mails — o que sai e o que fica. O que fazer com isso é decisão doibft-backend: se o e-mail que fica já existe lá, é fusão; se não existe, é troca de e-mail. Mesmo host e mesmo token do contrato de reparcelamento — muda só o path.
Por que este endpoint existe
No checkout-api o cliente é um cadastro por e-mail, com unicidade garantida na aplicação, 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 — comprou com um e-mail, depois com outro — 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 hoje para os sistemas a jusante, e o formato varia: CBTRG e Onion recebem um webhook com esses dois e-mails, o Asaas é atualizado pela API do gateway e o Apolo por REST, buscando o usuário pelo e-mail antigo. O ibft-backend entra no primeiro grupo — o mesmo par de e-mails — porque 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.
E o e-mail que sai deixa de existir do lado de cá. Se ele continuar ativo lá, a próxima sincronização de reparcelamento desse cliente casa pelo e-mail antigo e recria a divisão.
A chamada é global, não leva company_slug. O mesmo cadastro pode ter pedidos em mais de uma empresa, e todos mudam de dono na mesma operação.
Quando dispara
Gatilho no checkout-api |
O que vai no payload |
|---|---|
Unificação de dois cadastros — ação “Unificar” no admin ou IbftMergeCustomersJob |
email = cadastro que sai, new_email = cadastro que fica |
| Troca de e-mail de um cadastro existente | email = endereço antigo, new_email = endereço novo |
Os dois casos mandam o mesmo payload. A origem não distingue um do outro e o ibft-backend não precisa de um flag para isso: a diferença é observável lá, pelo new_email já existir ou não.
O envio é assíncrono: o job é enfileirado dentro da transação da unificação e, como a fila é o próprio banco (GoodJob), só fica executável depois do commit — transação revertida não gera chamada. Diferente do Onion, que só recebe quando o cliente tem pagamento com aquela integração, este endpoint recebe toda unificação e toda troca de e-mail: filtrar na origem exigiria ela saber o que já foi sincronizado lá, e é justamente para isso que existe a resposta noop.
Há um caminho manual de unificação pelo console que pula a propagação (
run_other_services: false). Quem roda assim assume o sincronismo — nenhuma chamada sai.
A troca de e-mail simples não é opcional: o e-mail é a chave de deduplicação do comprador nos dois lados, e um endereço que muda só aqui faz o próximo reparcelamento criar um comprador duplicado lá.
Requisição
Endpoint
POST /v1/sync/user
Content-Type: application/json
É para onde a origem já aponta. Fica sob o mesmo prefixo /v1/sync/ do reparcelamento, e precisa ser estável — mudança de path exige deploy coordenado dos dois lados.
Autenticação
Mesmo token do contrato de reparcelamento, no header Authorization: Bearer <token>. Do lado da origem, os dois contratos leem as mesmas variáveis de ambiente (NEW_CHECKOUT_API_URL e NEW_CHECKOUT_API_TOKEN) — nenhuma configuração nova para este endpoint. Mesmas regras: só HTTPS, nunca em query string, fora do log de aplicação, rotacionável sem redeploy coordenado dos dois lados. Fail-closed: sem token configurado no servidor, rejeitar a requisição em vez de liberar.
JSON de entrada
json
{
"email": "joao.silva@example.com",
"new_email": "joao@example.com"
}
| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
email |
string | sim | Endereço que sai: o cadastro desativado na unificação, ou o e-mail antigo na troca |
new_email |
string | sim | Endereço que fica: o cadastro correto, ou o e-mail novo |
Invariante garantida pela origem: email é diferente de new_email, e os dois vêm em minúsculas.
O que precisa ser feito
Em resultado, não em código:
- Autenticar pelo mesmo token compartilhado. Sem token configurado no servidor, rejeitar.
- Localizar o comprador pelo
email. Não existe lá → nada a fazer, responder200comnoop: é o caso comum de um cliente que nunca teve pedido sincronizado. - Decidir pelo
new_email:- já existe um comprador com ele → é fusão. Mover para esse comprador tudo que aponta para o do
email— pedidos, parcelas, vínculos com empresa, o que mais houver — e liberar oemail; - não existe → é troca. O comprador do
emailpassa a ternew_email.
- já existe um comprador com ele → é fusão. Mover para esse comprador tudo que aponta para o do
- Liberar o endereço que saiu: depois da operação,
emailnão pode mais resolver para um comprador ativo. Desativar, renomear, apagar — a forma é decisão de vocês, o resultado não. - Ser atômico: ou tudo muda de dono, ou nada muda. Um merge pela metade parte a dívida do cliente em dois compradores sem deixar rastro de que são a mesma pessoa.
- Ser idempotente, e aqui isso sai de graça: depois da primeira chamada o
emailnão resolve mais, então o retry cai no passo 2 e não faz nada. Por isso o payload não carrega chave de idempotência. - Não produzir nenhum efeito colateral: nenhum e-mail ao cliente, nenhuma confirmação de endereço, nenhum reset de senha, nenhuma liberação ou revogação de acesso, nenhuma comissão, nenhum evento de pedido. A operação é administrativa e os pedidos envolvidos são de compras passadas.
- Não chamar o Asaas. O
checkout-apijá atualiza o cliente no gateway na mesma operação; uma segunda chamada lá só gera divergência.
Resposta
200 OK
json
{
"status": "merged",
"customer_code": "6b1f3a92-5c4d-4e77-8a10-b2c3d4e5f6a7",
"orders_moved": 3
}
| Campo | Descrição |
|---|---|
status |
merged (fusão), updated (troca de e-mail) ou noop (o email não existe lá) |
customer_code |
Código do comprador que ficou. null no noop |
orders_moved |
Pedidos que mudaram de dono. 0 em updated e em noop |
401 Unauthorized — token ausente ou inválido
json
{ "detail": "Não autorizado." }
422 Unprocessable Entity
Mesmo formato do endpoint de reparcelamento — detail, code e errors por campo. Um code só aqui: invalid_payload, para e-mail ausente, malformado ou igual ao outro.
500 Internal Server Error
json
{ "detail": "Erro interno." }
Política de retry da origem
A mesma do contrato de reparcelamento: 4xx (exceto 429) é falha definitiva, com alerta e intervenção manual. Do lado do checkout-api, esse retry é feito pelo outbox local — até 3 tentativas, espaçadas em 5 segundos pelo retry_on do job.
O que isso exige de cada lado
De vocês: o endpoint, a decisão fusão-ou-troca pelo new_email, e a garantia de que mover o dono do pedido não dispara nada — nem notificação, nem acesso, nem comissão.
Da gente: disparar do mesmo ponto em que hoje sincronizamos e-mail com Asaas, CBTRG, Apolo e Onion (IbftEmailSyncService), reaproveitando o payload que já é montado lá — feito, via OutboxEvent com event_name: "new_checkout.customer_sync".