Desenvolvimento Salesforce

Como calcular desconto por volume no Salesforce sem sair da Oportunidade

Banner do artigo sobre calcular desconto por volume no Salesforce direto na Oportunidade

A diretoria aprovou a tabela: acima de cem unidades, cinco por cento; acima de seiscentas, doze. Bonito no slide. Na prática, cada vendedor guarda a faixa na cabeça de um jeito, arredonda para cima quando quer fechar o mês, e no fim do trimestre você tem três descontos diferentes para o mesmo volume, todos digitados à mão na Oportunidade. A política existe. O que não existe é o lugar onde ela é aplicada sozinha.

Para aplicar desconto por volume no Salesforce sem digitar à mão, você guarda as faixas de quantidade em um Custom Metadata Type e coloca uma automação antes de salvar na OpportunityLineItem que lê a quantidade da linha e preenche o campo Discount. A faixa vira configuração que o admin edita e o cabeçalho é derivado das linhas.

Por que deixar o vendedor digitar o desconto quebra a política

O problema do desconto na unha não é o vendedor mal-intencionado, é a ausência de um ponto único onde a regra vale. Quando o percentual é um campo em branco esperando ser preenchido, ele vira negociação interna: um vendedor aplica os cinco por cento da faixa, outro estica para sete porque o cliente reclamou, um terceiro esquece e manda sem desconto nenhum. Nenhum deles está errado sozinho, porque não há nada no sistema dizendo qual é o certo. A tabela aprovada mora num PDF que ninguém abre na hora de fechar.

O efeito aparece na margem. Você projeta a receita líquida assumindo a política oficial, e o realizado vem abaixo porque metade das oportunidades saiu com desconto acima da faixa. Quando o financeiro cruza os números, a diferença já foi assinada pelo cliente. Colocar a regra dentro do CRM não é sobre controlar o vendedor, é sobre a empresa ter uma resposta só para a pergunta "qual desconto essa quantidade recebe".

Onde o desconto mora: na linha, não no cabeçalho

O primeiro erro de modelagem é achar que o desconto é um atributo da Oportunidade. Não é. A Oportunidade guarda o cabeçalho (cliente, estágio, valor total), e cada produto vendido é um registro de OpportunityLineItem, filho, com sua própria quantidade, preço e o campo Discount em percentual. O desconto por volume é, por natureza, uma decisão por linha: você comprou seiscentas válvulas, essa linha ganha a faixa de seiscentas; comprou oitenta conexões, aquela linha fica na faixa de baixo. São descontos diferentes na mesma Oportunidade.

A consulta que a automação usa para reagir às linhas alteradas roda no filho, e roda respeitando o que o usuário pode ver:

List<OpportunityLineItem> linhas = [
    SELECT Id, Quantity, UnitPrice, Discount, Product2.Family
    FROM OpportunityLineItem
    WHERE OpportunityId = :oppId
    WITH USER_MODE
];

Esse WITH USER_MODE não é decoração: a classe que mexe em dados de negociação respeita CRUD e FLS do usuário logado, como qualquer código que devolve ou grava registro de tela. O porquê disso, e o que o Apex ignora por padrão quando você não pede, eu destrinchei no post sobre segurança em Apex com CRUD, FLS e sharing. Guarde a regra do modelo antes de seguir: o desconto vive na linha, e o número do cabeçalho é reflexo dela, nunca o contrário.

As faixas viram configuração, não código

Se você escreve if (qtd >= 100) desconto = 5; dentro do Apex, cravou a política de comercial no código. Toda vez que a diretoria mexer na tabela, e ela mexe a cada campanha, você precisa de um deploy. A saída é tirar os números do código e colocá-los em um Custom Metadata Type, digamos Faixa_Desconto__mdt, com três campos: quantidade mínima, percentual e, se a política variar por linha de produto, a família de produto a que a faixa se aplica.

Registro (mdt)Qtd. mínimaDescontoFamília
Padrao_1001005%(todas)
Padrao_3003008%(todas)
Padrao_60060012%(todas)

Com as faixas em metadado, mudar a política é editar um registro em Setup: sem deploy, sem release, sem abrir chamado para o desenvolvedor. O administrador de comercial passa a ser dono da tabela, que é onde ela deveria estar. A automação só faz uma coisa: pega a quantidade da linha, procura a maior faixa cuja quantidade mínima seja menor ou igual a ela, e aplica o percentual daquela faixa. Em Apex, o coração do cálculo é este laço, com as faixas ordenadas da menor para a maior quantidade:

Decimal descontoDaFaixa(Integer qtd, List<Faixa_Desconto__mdt> faixas) {
    Decimal aplicado = 0;
    for (Faixa_Desconto__mdt f : faixas) {          // ordenadas por Qtd_Minima__c asc
        if (qtd >= f.Qtd_Minima__c) {
            aplicado = f.Desconto__c;               // a última que passa é a maior faixa
        }
    }
    return aplicado;
}

Repare que o laço não para na primeira faixa que bate: ele continua e vai sobrescrevendo, então termina com o percentual da maior faixa que a quantidade alcança. É um detalhe bobo que, invertido, faz oitocentas unidades receberem os cinco por cento da faixa de cem em vez dos doze por cento da faixa de seiscentas. O bug não estoura, ele só entrega dinheiro a menos de desconto, e ninguém percebe até o cliente reclamar.

A conta feita na frente: três linhas, três faixas

A parte que engana é achar que o desconto do cabeçalho é a média dos percentuais das linhas. Não é. Ele é ponderado pelo valor, e a única forma de acertar é derivá-lo. Veja com os números de uma Oportunidade da Vetra, a distribuidora fictícia que mantenho como demonstração do portfólio, usando as faixas da tabela acima:

ProdutoQtdPreço unit.BrutoFaixaDesc.Líquido
Filtro industrial A250R$ 50,00R$ 12.500100+5%R$ 11.875
Válvula B600R$ 120,00R$ 72.000600+12%R$ 63.360
Conexão C80R$ 12,50R$ 1.000< 1000%R$ 1.000
TotalR$ 85.50010,84%R$ 76.235

O desconto do cabeçalho é 10,84 por cento, e não a média simples de 5, 12 e 0 (que daria 5,67). A conta é o desconto total em dinheiro, R$ 9.265, dividido pelo bruto de R$ 85.500. A linha da válvula domina o número porque ela sozinha vale mais que as outras duas somadas, então o desconto agregado puxa para perto dos doze por cento dela. Se você deixasse alguém digitar "8%" no cabeçalho por achatamento, a proposta diria uma coisa e a soma das linhas diria outra. Por isso o agregado é derivado, no mesmo princípio de fonte única que uso para gerar a proposta comercial em PDF direto da Oportunidade: o total mora nas linhas e o cabeçalho é uma projeção.

Flow antes de salvar ou trigger Apex?

Para o caso mais comum, desconto pela quantidade da própria linha, um Flow before-save na OpportunityLineItem é a ferramenta mais barata: roda antes de gravar, sem DML extra, e um admin consegue manter. Ele lê Quantity, busca a faixa no Custom Metadata (Flow consulta registros de CMDT nativamente) e escreve em Discount. Nenhuma linha de Apex, nenhum custo de governor limit por atualização.

A escolha vira Apex quando a regra deixa de olhar só para a linha. Se a faixa depende da soma de todas as linhas do pedido (comprou R$ 80 mil no total, ganha a faixa cheia, mesmo pulverizado em itens pequenos), um before-save por linha não serve, porque ele não enxerga as irmãs. Aí você precisa de lógica no nível da Oportunidade, disparada quando qualquer linha muda, somando o pedido inteiro e reaplicando o desconto em cada linha. Isso é território de trigger Apex bulkificado, não de Flow por linha. A fronteira entre os dois é a mesma que tracei no post sobre quando usar Flow ou Apex no Salesforce: enquanto a regra couber na linha, Flow; quando ela precisa de contexto de várias linhas, cálculo em lote e ordem de execução previsível, Apex.

O caso da Vetra e a armadilha do cabeçalho sobrescrito

A primeira versão que montei na Vetra tinha um campo Desconto_Aplicado__c no cabeçalho da Oportunidade, calculado por um rollup a partir das linhas. Funcionava, até eu tentar "ajudar" preenchendo também um valor manual no mesmo campo em um cenário de teste. O rollup recalculou no salvamento seguinte e apagou o que eu tinha digitado, sem aviso. Levei um tempo bom desconfiando da automação de linha antes de cair a ficha: o problema não era a linha, era eu escrevendo em um campo que já tinha um dono, o rollup.

A lição virou regra de projeto: o desconto se seta na linha (OpportunityLineItem.Discount), nunca no agregado do cabeçalho. O número do cabeçalho é sempre derivado, seja por rollup, seja recalculado na hora de renderizar. No instante em que dois lugares tentam ser a fonte do mesmo desconto, um deles vai sobrescrever o outro em silêncio, e você vai gastar a tarde procurando o bug no lugar errado. Esse princípio de eleger um dono e derivar o resto eu generalizei no post sobre como definir uma fonte de verdade única no Salesforce, que trata do caso em que dois campos disputam o mesmo desconto. Fonte única não é purismo de arquitetura, é o que impede a proposta e o forecast de contarem histórias diferentes.

Quando não vale a pena construir isso

Ser honesto aqui vale mais que empurrar horas. Se a sua empresa já tem Salesforce CPQ, não construa nada: o CPQ traz Discount Schedules nativos, feitos exatamente para desconto por volume e por prazo, com faixas configuráveis na interface e cálculo pronto na cotação. Reinventar isso em Custom Metadata seria trabalho jogado fora. A solução deste post é para quem não tem CPQ e não vai pagar a licença só por causa de uma tabela de faixas.

E se a sua realidade é desconto negociado caso a caso, sem política fixa de volume, talvez você não precise de automação nenhuma: um campo de desconto com uma validation rule travando o teto por perfil já resolve, e é mais barato de manter que uma engine de faixas que ninguém vai alimentar. Construa a regra quando a política existe e é estável o bastante para virar tabela. Se ela muda a cada negociação, o que você tem é exceção, não faixa, e exceção não se automatiza, se governa com limite e alçada.

Perguntas frequentes

Dá para aplicar desconto por volume no Salesforce sem CPQ?

Sim. As faixas de quantidade ficam num Custom Metadata Type e uma automação before-save na OpportunityLineItem lê a quantidade e preenche o Discount. O CPQ traz Discount Schedules prontos, mas é produto pago; para faixas simples, Custom Metadata e um before-save resolvem sem licença extra.

O desconto por volume vai no cabeçalho ou na linha?

Na linha. Cada OpportunityLineItem tem seu Discount, calculado pela faixa da quantidade dela. O desconto do cabeçalho é derivado (desconto em dinheiro sobre o bruto), nunca um campo digitado à parte, senão a proposta e o forecast divergem.

A faixa é pela quantidade da linha ou pelo total do pedido?

Depende da política, e muda a arquitetura. Faixa pela quantidade de cada linha cabe num before-save na própria linha. Faixa pela soma do pedido precisa enxergar as linhas irmãs, o que exige Apex no nível da Oportunidade ou um rollup, não um before-save por linha.

Como mudo as faixas sem chamar o desenvolvedor?

Se as faixas moram em registros de Custom Metadata Type, o admin edita quantidade mínima e percentual em Setup, sem deploy. É por isso que a regra vai em metadado e não cravada no Apex ou no Flow: a política de comercial muda toda campanha.

Segunda opinião técnica

Sua org cresceu. Os riscos também?

Revisão independente de automações, integrações e pontos frágeis, com prioridades claras para decidir o próximo passo.

Entender o diagnóstico →