Arquitetura Salesforce

Dois campos para o mesmo conceito: como definir uma fonte de verdade única no Salesforce

Banner do artigo sobre definir uma fonte de verdade única de um campo no Salesforce

O gestor abriu o relatório de margem numa terça e o desconto médio da carteira estava em 9,2 por cento. Na proposta que ele tinha assinado na sexta, o mesmo cliente aparecia com 11 por cento. Mesmo negócio, dois números, e a pergunta óbvia caiu no meu colo: qual dos dois está certo? A resposta desconfortável era "os dois", porque a org guardava o desconto em dois campos diferentes, alimentados por dois motores que nasceram em sprints diferentes e nunca conversaram.

Para definir uma fonte de verdade única no Salesforce, elege um único campo como dono do conceito, faz todos os outros lugares derivarem dele em vez de guardar cópias próprias, e trava a escrita nos campos derivados. No desconto, a verdade mora na linha da Oportunidade e o cabeçalho é uma média ponderada dela, nunca um valor digitado à parte.

Como um CRM acaba com dois campos para a mesma coisa

Ninguém desenha isso de propósito. O modelo apodrece por incremento. No primeiro trimestre, alguém pede um campo de desconto na Oportunidade para o vendedor registrar o abatimento que negociou, e nasce um Discount_Percent__c no cabeçalho, digitado à mão. Dois trimestres depois, chega a demanda de desconto por volume na linha, e o desenvolvedor da vez usa o campo nativo Discount da OpportunityLineItem, que é onde o Salesforce espera que o desconto de item viva. Cada decisão, isolada, é razoável. Juntas, elas criam dois lugares para o mesmo conceito.

A partir daí, o estrago é questão de tempo. O relatório de comissão que o financeiro montou lê o campo do cabeçalho, porque era o que existia quando ele foi feito. O PDF da proposta calcula a partir das linhas, porque foi construído depois. A regra de alçada que barra desconto acima de 15 por cento olha para um dos dois, e ninguém lembra qual. Enquanto os valores por acaso batem, tudo parece funcionar. No dia em que um vendedor aplica desconto na linha sem tocar no cabeçalho, os dois campos divergem, e cada tela da empresa passa a contar uma história diferente sobre o mesmo negócio.

O sintoma aparece no relatório, a doença está no modelo

Quase sempre a divergência é descoberta por um relatório que não fecha, e o primeiro impulso é culpar o relatório. Você revisa o filtro, confere o agrupamento, desconfia da fórmula de bucket, e não acha nada errado, porque não há nada errado ali. O relatório está fielmente somando um campo que contém um número desatualizado. O problema não é a soma, é o fato de existir um campo que pode ficar desatualizado em relação à verdade sem que nada avise.

Esse é o teste que uso para saber se estou diante de um problema de fonte de verdade: se eu apagasse o valor deste campo agora, eu conseguiria recalculá-lo a partir de outros dados da org? Se a resposta é sim, o campo não deveria guardar um valor próprio, deveria derivá-lo. O desconto do cabeçalho é o exemplo perfeito. Ele é integralmente reconstruível a partir das linhas: basta o desconto em dinheiro de cada uma e o valor bruto de cada uma. Um campo que pode ser recalculado mas guarda uma cópia digitada é uma divergência esperando a hora de acontecer.

Eleger a fonte: o desconto mora na linha

Entre os dois candidatos, a escolha não é arbitrária. A fonte de verdade deve ser o campo mais próximo de onde o dado nasce, e o desconto nasce por item: cada produto tem sua quantidade, seu preço e sua faixa. O cabeçalho é um resumo, e resumo é sempre derivado do detalhe, nunca o contrário. Então a regra do projeto vira uma frase só: a verdade do desconto é o Discount de cada OpportunityLineItem, e o número do cabeçalho é uma projeção dele. Como as faixas de volume chegam nesse campo da linha eu destrinchei no post sobre desconto por volume no Salesforce sem sair da Oportunidade; aqui o assunto é o passo seguinte, garantir que todo o resto da org concorde com esse número.

Eleita a fonte, o cabeçalho precisa de um valor, porque relatório de diretoria, alçada e a engine de comissão que roda sobre essas linhas querem um número único por Oportunidade, não uma lista de linhas. A tentação é preencher esse número com a média dos percentuais das linhas. É aqui que quase todo mundo erra a conta.

A conta feita na frente: desconto efetivo ponderado

O desconto agregado de uma Oportunidade é uma média ponderada pelo valor de cada linha, não a média simples dos percentuais. A diferença não é sutil, ela muda o número que vai para a margem. Veja com três linhas de uma Oportunidade da Vetra, a distribuidora fictícia que mantenho como demonstração do portfólio:

ProdutoBrutoDesconto da linhaDesconto em R$Líquido
Filtro industrial AR$ 12.5005%R$ 625R$ 11.875
Válvula BR$ 72.00012%R$ 8.640R$ 63.360
Conexão CR$ 1.0000%R$ 0R$ 1.000
TotalR$ 85.50010,84%R$ 9.265R$ 76.235

A média simples dos percentuais daria 5,67 por cento, a soma de 5, 12 e 0 dividida por três. O desconto efetivo real é 10,84 por cento: os R$ 9.265 de desconto em dinheiro divididos pelos R$ 85.500 de bruto. A distância entre 5,67 e 10,84 é quase o dobro, e ela existe porque a linha da válvula sozinha vale mais que as outras duas somadas, então o desconto dela domina o agregado. Se o cabeçalho guardasse a média simples, a empresa acharia que dá metade do desconto que realmente dá. É a diferença entre projetar a margem certa e descobrir o rombo depois que o cliente assinou.

Por isso o campo do cabeçalho não pode ser digitado nem calculado por média ingênua: ele tem que ser derivado pela fórmula do ponderado. Some o desconto em dinheiro das linhas, some o bruto das linhas, divida um pelo outro. Esse é o mesmo princípio de agregado derivado que uso para montar a proposta comercial em PDF a partir da Oportunidade: o total mora nas linhas e o cabeçalho é sempre um reflexo calculado.

O rollup que mantém a derivação viva

Um número derivado só serve se ele se atualiza sozinho toda vez que a fonte muda. Se alguém precisa clicar em "recalcular", você não tem uma derivação, tem outra cópia manual com passo extra. Como o desconto ponderado depende da soma das linhas e envolve divisão, um Roll-Up Summary nativo não dá conta sozinho (ele soma e conta, mas não faz a razão ponderada), então o caminho é um rollup em trigger Apex na OpportunityLineItem que, a cada mudança de linha, recalcula o cabeçalho da Oportunidade pai. O núcleo é este, bulkificado para tratar várias Oportunidades de uma vez:

Map<Id, Decimal> brutoPorOpp = new Map<Id, Decimal>();
Map<Id, Decimal> descontoPorOpp = new Map<Id, Decimal>();

for (OpportunityLineItem oli : [
        SELECT OpportunityId, Quantity, UnitPrice, Discount
        FROM OpportunityLineItem
        WHERE OpportunityId IN :oppIds
        WITH USER_MODE]) {
    Decimal bruto = oli.Quantity * oli.UnitPrice;
    Decimal desc  = bruto * (oli.Discount == null ? 0 : oli.Discount) / 100;
    brutoPorOpp.put(oli.OpportunityId,
        (brutoPorOpp.get(oli.OpportunityId) == null ? 0 : brutoPorOpp.get(oli.OpportunityId)) + bruto);
    descontoPorOpp.put(oli.OpportunityId,
        (descontoPorOpp.get(oli.OpportunityId) == null ? 0 : descontoPorOpp.get(oli.OpportunityId)) + desc);
}

List<Opportunity> paraAtualizar = new List<Opportunity>();
for (Id oppId : oppIds) {
    Decimal bruto = brutoPorOpp.get(oppId);
    Decimal efetivo = (bruto == null || bruto == 0) ? 0
        : (descontoPorOpp.get(oppId) / bruto * 100).setScale(2);
    paraAtualizar.add(new Opportunity(Id = oppId, Discount_Percent__c = efetivo));
}
update as user paraAtualizar;

Três detalhes salvam esse código de virar bug. O WITH USER_MODE e o update as user fazem o rollup respeitar CRUD e FLS do usuário que disparou o salvamento, em vez de escrever à revelia da permissão dele, um cuidado que detalhei no post sobre segurança em Apex com CRUD, FLS e sharing. A guarda de bruto == 0 evita a divisão por zero quando a Oportunidade fica sem linhas. E o acúmulo em Map por Oportunidade é o que mantém o trigger bulkificado: uma importação que mexe em mil linhas de duzentas Oportunidades roda em duas queries, não em mil.

A decisão de fazer isso em Apex e não em Flow segue a mesma fronteira de sempre. Enquanto o cálculo cabe na própria linha, Flow before-save resolve barato; quando ele precisa enxergar as linhas irmãs e reescrever o pai de forma agregada, é território de trigger, como argumentei no post sobre quando usar Flow ou Apex no Salesforce.

Campo derivado contra campo digitado: trave a porta dos fundos

Derivar o cabeçalho não basta se o campo continuar editável. Enquanto Discount_Percent__c aceitar digitação, alguém vai digitar, e no salvamento seguinte o rollup vai sobrescrever, gerando aquela sensação de "eu mudei e o sistema desfez". A regra que fecha o ciclo é: campo derivado é campo somente leitura. Tire-o do page layout como editável, deixe-o como read-only, ou melhor, remova o acesso de escrita pelo perfil e pelo permission set, para que nem via API alguém grave nele por engano. A única mão que escreve ali é a do rollup.

Vale a distinção que separa os dois tipos de campo, porque ela decide o modelo inteiro. Campo digitado é entrada original: a quantidade que o vendedor negociou, o preço de tabela, o desconto de item que ele concedeu. Ninguém consegue recalcular isso a partir de outra coisa, então tem que ser digitado, e é ali que mora a verdade. Campo derivado é consequência: o desconto agregado, o valor líquido total, a margem. Se dá para calcular, não se digita, se deriva e se tranca. Todo campo que você deixa aberto para digitação sendo, no fundo, derivável, é um segundo dono para um dado que só pode ter um.

O caso da Vetra: o dia que o painel mudou sozinho

A primeira versão que montei na Vetra tinha o Discount_Percent__c do cabeçalho alimentado pelo rollup, o que estava certo, mas eu tinha deixado o campo editável no layout, o que estava errado. Num teste, "ajudei" digitando 8 por cento à mão numa Oportunidade para conferir um relatório. O número apareceu bonito no painel do gestor. Salvei uma alteração de linha minutos depois, o trigger rodou, e o painel voltou para 10,84 sozinho, sem aviso. Passei um tempo bom desconfiando do relatório e do cache do dashboard antes de cair a ficha de que o problema era eu, escrevendo num campo que já tinha dono.

A correção não foi de código, foi de disciplina de modelo: tirei o acesso de escrita do campo, deixei só o rollup como autor, e a divergência entre proposta e relatório sumiu na origem. Ninguém mais consegue criar um segundo número, porque só existe um lugar onde o desconto pode ser gravado. Foi a lição que virou regra de projeto e o motivo deste post existir: o custo de eleger a fonte de verdade é uma tarde de configuração; o custo de não eleger é uma divergência silenciosa que você descobre na frente do cliente.

Quando você não precisa de rollup nenhum

Ser honesto aqui poupa você de construir peso morto. Se o seu desconto vive num campo só, sem um segundo concorrente, você não tem um problema de fonte de verdade, tem um campo, e está tudo certo. Não invente o cabeçalho derivado só porque leu que dá. A engine de rollup só se justifica quando existem de fato duas leituras do mesmo conceito que precisam ser reconciliadas, ou quando a diretoria consome o agregado num relatório e ele precisa estar sempre certo.

E se o número derivado couber numa expressão que enxerga só o próprio registro, prefira um formula field a um trigger: ele é derivado por natureza, não guarda cópia, não estoura governor limit e não precisa de teste. O rollup em Apex só entra quando a derivação depende de somar registros filhos de forma ponderada, como o desconto efetivo deste post, que uma fórmula não alcança. Construa a máquina no tamanho do problema: um dado, um dono, e a menor engrenagem que mantém os dois de pé.

Sua org tem dois números para o mesmo dado?

Eu faço o diagnóstico do modelo, elejo a fonte de verdade, monto o rollup derivado com o teste que sobe sem quebrar e tranco os campos que não podem ter dois donos. Comece com um diagnóstico gratuito de 45 minutos.

Falar no WhatsApp Ver serviços

Perguntas frequentes

O que é uma fonte de verdade única no Salesforce?

É a decisão de que um conceito de negócio mora em um único campo, e todos os outros lugares que precisam daquele número o derivam desse campo em vez de guardar uma cópia própria. Se o desconto é a verdade na linha da Oportunidade, o cabeçalho, o relatório e a comissão leem esse número derivado, nunca um campo digitado à parte que pode divergir.

Como calcular o desconto efetivo de uma Oportunidade com várias linhas?

O desconto efetivo é a média ponderada pelo valor de cada linha, não a média simples dos percentuais. Some o desconto em dinheiro de todas as linhas, some o valor bruto de todas as linhas, e divida o primeiro pelo segundo. Uma linha grande com desconto alto puxa o número agregado mais que uma linha pequena, o que a média simples ignora.

Campo derivado ou campo digitado: qual usar?

Use campo digitado só quando o dado é uma entrada original que ninguém recalcula, como a quantidade que o vendedor negociou. Quando o número pode ser calculado a partir de outros campos, como o desconto agregado do cabeçalho, ele deve ser derivado por fórmula ou rollup e ficar somente leitura, para que não exista um segundo lugar onde alguém digite um valor divergente.

Formula field ou rollup no trigger para derivar o campo?

Se o número cabe numa expressão que enxerga só o próprio registro, um formula field resolve sem código. O desconto agregado do cabeçalho depende da soma ponderada das linhas filhas, que uma fórmula não alcança, então ele exige um rollup: Roll-Up Summary quando os campos permitem, ou um trigger Apex bulkificado quando o cálculo é ponderado.