Para nunca duplicar, cada entidade precisa de uma chave que ligue o registro do CRM ao do ERP. Guarde esse vinculo numa mapping table: external_id de um lado, external_id do outro. Toda escrita vira um upsert por essa chave, nao um insert cego. Assim, reprocessar o mesmo evento dez vezes produz o mesmo resultado: idempotencia.
- Mapping table: tabela que relaciona crm_id, erp_id e a chave natural (CNPJ, SKU) para resolver o par sem ambiguidade.
- Chave natural: quando nao ha mapping ainda, casa pelo CNPJ do cliente ou SKU do produto, normalizados antes de comparar.
- Upsert por chave: insere se nao existe, atualiza se existe. Nunca um insert direto que cria duplicata em retry.
- Idempotencia: a mesma mensagem aplicada N vezes deixa o sistema no mesmo estado, essencial porque webhook reenvia.
// sync-customer.js
// Upsert idempotente de cliente entre CRM e ERP usando mapping table.
// Roda no middleware ao receber um evento "customer.updated" do CRM.
async function syncCustomerFromCrm(event) {
const crmId = event.data.id;
const cnpj = normalizeCnpj(event.data.cnpj); // remove pontuacao, valida
// 1. Resolve o par via mapping table; cai para chave natural se nao houver vinculo
let mapping = await db.mapping.findOne({ crm_id: crmId });
if (!mapping) {
const erp = await erpApi.findCustomerByCnpj(cnpj);
if (erp) {
// Cliente ja existe no ERP, so faltava o vinculo: nao duplica
mapping = await db.mapping.upsert({ crm_id: crmId, erp_id: erp.id, cnpj });
}
}
// 2. Monta apenas os campos que o CRM e dono (contato e proposta)
const payload = {
nome: event.data.nome,
email: event.data.email,
telefone: event.data.telefone,
};
// 3. Upsert idempotente: chave de negocio = erp_id (se existe) ou CNPJ
const erpCustomer = await erpApi.upsertCustomer({
matchBy: mapping?.erp_id ? { id: mapping.erp_id } : { cnpj },
data: payload,
// dedupe_key garante que reprocessar o mesmo evento nao gere efeito duplo
dedupeKey: `crm:${crmId}:${event.version}`,
});
// 4. Persiste/atualiza o vinculo para a proxima sincronizacao
await db.mapping.upsert({ crm_id: crmId, erp_id: erpCustomer.id, cnpj });
return erpCustomer.id;
}
Repare que o codigo nunca faz insert direto: ele resolve o par antes, casa por CNPJ quando o vinculo ainda nao existe e usa um dedupeKey baseado na versao do evento. Esse trio (mapping, chave natural e dedupe) e o que mata a duplicata na origem.