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ínima | Desconto | Família |
|---|---|---|---|
| Padrao_100 | 100 | 5% | (todas) |
| Padrao_300 | 300 | 8% | (todas) |
| Padrao_600 | 600 | 12% | (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:
| Produto | Qtd | Preço unit. | Bruto | Faixa | Desc. | Líquido |
|---|---|---|---|---|---|---|
| Filtro industrial A | 250 | R$ 50,00 | R$ 12.500 | 100+ | 5% | R$ 11.875 |
| Válvula B | 600 | R$ 120,00 | R$ 72.000 | 600+ | 12% | R$ 63.360 |
| Conexão C | 80 | R$ 12,50 | R$ 1.000 | < 100 | 0% | R$ 1.000 |
| Total | R$ 85.500 | 10,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.
Quer a política de desconto por volume aplicada sozinha na Oportunidade?
Eu monto o Custom Metadata das faixas, a automação na linha e o desconto do cabeçalho derivado, com o teste que sobe sem quebrar e o admin no controle da tabela. Comece com um diagnóstico gratuito de 45 minutos.
Falar no WhatsApp Ver serviçosPerguntas 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.
