Regras de Campanhas
Este documento é a fonte única da verdade para as regras de negócio de acúmulo, resgate e validade de créditos do EloFiel. Sempre que uma regra de cálculo mudar no backend, este documento, os textos de ajuda do formulário de campanha (campanha-form.component.ts) e os testes de integração devem ser atualizados juntos.
1. Seleção de Campanha — Categoria de Benefício (issue #3)
Quando uma venda é registrada (POST /api/v1/vendas), o sistema seleciona todas as campanhas ativas elegíveis e aplica uma por uma. Cada campanha aplicada gera uma movimentação independente — todas compartilham o mesmo VendaId para fins de cancelamento bulk.
var elegiveis = campanhasAtivas
.Where(c => valorVenda >= c.CompraMinima)
.OrderBy(c => c.Prioridade descending, c.CreatedAt ascending)
.ToList();
foreach (var campanha in elegiveis) {
aplicarCampanha(campanha); // gera 1 movimentação + 1 saldo_lote
}
Regra de unicidade
Por loja (considerando EscopoLojas), só pode haver 1 campanha ativa de cada Tipo:
| Combinação ativa | Aceito? |
|---|---|
| 1 Cashback + 1 Pontos | ✅ Padrão de mercado (Itaú Iupp, Inter Loop) |
| Apenas 1 Cashback | ✅ |
| Apenas 1 Pontos | ✅ |
| 2 Cashback simultâneas na mesma loja | ❌ Rejeitado com 422 — desative uma primeiro |
| 2 Pontos simultâneas na mesma loja | ❌ Rejeitado com 422 — desative uma primeiro |
A validação é feita pelo CampanhaAdminService no momento de criar ou ativar a campanha. Mensagem padrão:
"Já existe uma campanha de <cashback|pontos> ativa nesta loja ("..."). Desative-a primeiro ou ajuste a data de fim antes de ativar outra."
A regra "máximo 1 Cashback + 1 Pontos por loja" é fixa para todos os tenants e não é configurável. Não existe flag de admin para "desligar" o limite ou permitir mais campanhas simultâneas. Lojistas que precisem de modelo diferente (campanha única, stacking custom, etc.) devem abrir issue para avaliação em Fase 2 — ver referências da issue #3.
Loop de aplicação por venda
Quando há 1 cashback + 1 pontos elegíveis para o mesmo valor:
- A campanha de Cashback governa o uso de saldo (Modelo 2) e o desconto sobre o cashback gerado (Modelo 3).
- A campanha de Pontos não tem regra de resgate de cashback — ela só audita o valor e gera pontos.
- A base de cálculo é uniforme: quando há desconto E a campanha de Cashback tem
permiteCashbackEmResgate = true, ambas usam o valor líquido; senão ambas usam o valor bruto.
Resposta da venda
VendaResponse traz a lista beneficiosAplicados[] com 1 ou 2 entries (uma por campanha aplicada). Campos pontosCreditados e cashbackCreditado no topo da resposta são agregados da lista.
Campos salvos mas não aplicados (limitações atuais)
| Campo | Salvo no banco | Aplicado em runtime |
|---|---|---|
Prioridade | ✅ | ✅ — usada para ordenação determinística no loop |
DataInicio / DataFim | ✅ | ❌ — ativação/desativação é manual via Status |
2. Regras de Acúmulo
2.1 Tipo de Regra: ValorGasto
O crédito é calculado proporcionalmente ao valor gasto na venda.
Campanha tipo Cashback
cashback = (valorVenda / valorBase) × percentualCashback / 100
+ valorFixoCashback ← opcional
cashback = aplicar_arredondamento(cashback)
if (limiteMaximoVenda > 0):
cashback = min(cashback, limiteMaximoVenda)
Exemplo: Venda de R$ 150, valorBase R$ 100, percentual 5%, sem fixo, Truncar
cashback = (150 / 100) × 0,05 = R$ 7,50
Exemplo com fixo: +R$ 1,00 fixo
cashback = R$ 7,50 + R$ 1,00 = R$ 8,50
Campanha tipo Pontos
pontos = Math.Floor((valorVenda / valorBase) × pontosGerados)
// pontos são sempre inteiros — truncamento obrigatório
Exemplo: Venda de R$ 75, valorBase R$ 10, pontosGerados 1
pontos = Math.Floor(75 / 10 × 1) = Math.Floor(7,5) = 7 pontos
2.2 Tipo de Regra: PorCompra
Valor ou pontos fixos por transação, independente do valor vendido, desde que valorVenda >= compraMinima.
| Tipo de campanha | Campo utilizado | Cálculo |
|---|---|---|
| Cashback | cashbackPorCompra | Valor fixo em R$ por venda |
| Pontos | pontosPorCompra | Quantidade fixa de pontos por venda |
3. Regras de Arredondamento (Cashback)
Aplicado após o cálculo do cashback bruto.
Truncar (padrão recomendado)
Math.Floor(valor * 100) / 100
| Valor bruto | Resultado |
|---|---|
| R$ 1,239 | R$ 1,23 |
| R$ 7,505 | R$ 7,50 |
| R$ 0,999 | R$ 0,99 |
Arredondar
Math.Round(valor, 2, MidpointRounding.AwayFromZero)
| Valor bruto | Resultado |
|---|---|
| R$ 1,235 | R$ 1,24 |
| R$ 7,505 | R$ 7,51 |
| R$ 0,994 | R$ 0,99 |
Regra de negócio:
Truncarfavorece a empresa (nunca paga a mais).Arredondaré matematicamente convencional.
4. Modos de Cashback
Imediato
Lote gerado com status = Disponivel. O cliente pode resgatar imediatamente após a venda.
dataVenda: 2026-03-29
statusLote: Disponivel
dataLiberacao: null
Diferido
Lote gerado com status = Pendente e dataLiberacao = dataVenda + diasCarencia.
dataVenda: 2026-03-29
diasCarencia: 7
statusLote: Pendente
dataLiberacao: 2026-04-05
O lote fica bloqueado para resgate até atingir a dataLiberacao. Jobs automáticos ou consulta em tempo real liberam o saldo ao atingir a data.
5. Validade dos Créditos
Aplicada por lote (cada venda gera um lote independente).
| Tipo | Cálculo da expiração |
|---|---|
Dias | dataExpiracao = dataEmissao + dias |
DataFixa | dataExpiracao = dataFixaConfigurada (igual para todos os lotes) |
FimMes | dataExpiracao = último dia do mês de emissão |
Ciclo de vida do lote:
Pendente → Disponivel → Resgatado
↘ Expirado
6. Regras de Resgate
Critérios de elegibilidade (todos devem ser atendidos)
saldoDisponivel >= cashbackMinimo (Cashback)
pontosAcumulados >= pontosMinimos (Pontos)
valorVenda >= vendaMinima (opcional)
statusLote == Disponivel (lotes Pendente não são elegíveis)
Tipos de resgate
| Tipo | Comportamento |
|---|---|
DescontoValor | Desconto em R$ direto na venda |
DescontoPercentual | Percentual sobre o valor da venda |
Brinde | Recompensa física (sem desconto monetário) |
Conversão de Pontos → Desconto
desconto_R$ = pontosUsados / conversaoPontos
Exemplo: 150 pontos, conversao = 10 → R$ 15,00 de desconto
Cap de desconto por venda
if (limiteDescontoVenda > 0):
desconto = min(descontoSolicitado, limiteDescontoVenda)
Uso Parcial
permiteUsoParcial | Comportamento |
|---|---|
true (padrão) | Cliente pode usar qualquer parte do saldo |
false | O resgate precisa usar o saldo inteiro de uma vez |
6.3 Desconto sobre Cashback Gerado (Modelo 3)
Habilitado quando permiteDescontoSobreCashbackGerado: true na CampanhaRegraResgate. Disponível apenas para campanhas Tipo = Cashback com ModoCashback = Imediato.
Fluxo de cálculo (em POST /api/v1/venda/preview e na venda real):
1. Calcular cashback bruto como se fosse uma venda normal:
cashbackBruto = calcularCashback(valorVenda, regraAcumulo)
2. Verificar elegibilidade:
permiteAplicarDesconto = campanhaPermite
&& cashbackBruto > 0
&& valorVenda >= compraMinima
3. Calcular valor final com desconto:
valorComDesconto = valorVenda − cashbackBruto
valorComDesconto = max(0, valorComDesconto)
4. Calcular novo cashback sobre o desconto (se PermiteCashbackEmResgate = true):
cashbackSobreDesconto = calcularCashback(valorComDesconto, regraAcumulo)
// caso contrário, cashbackSobreDesconto = 0 (sem duplo benefício)
5. Na venda real com usarCashbackGerado = true:
descontoGerado = cashbackBruto
valorFinal = valorComDesconto
cashbackCreditado = cashbackSobreDesconto // 0 quando PermiteCashbackEmResgate = false
Exemplo (campanha 10%, PermiteCashbackEmResgate = false):
valorVenda: R$ 100,00
cashbackBruto: R$ 10,00 (10% de R$ 100)
valorComDesconto: R$ 90,00
cashbackSobreDesconto R$ 0,00 (não gera novo cashback — anti-duplo benefício)
Exemplo (campanha 10%, PermiteCashbackEmResgate = true):
valorVenda: R$ 100,00
cashbackBruto: R$ 10,00 (10% de R$ 100)
valorComDesconto: R$ 90,00
cashbackSobreDesconto R$ 9,00 (10% de R$ 90,00 — cashback sobre o valor final)
Motivos de bloqueio retornados em motivoBloqueio:
- Campanha não permite desconto sobre cashback gerado
- Compra abaixo do valor mínimo da campanha
- Nenhuma campanha ativa para a loja/valor
7. Checklist de Atualização de Regras
Sempre que uma regra de cálculo for alterada no backend, todos os itens abaixo devem ser atualizados na mesma PR:
- Backend —
VendaService.cs,ResgateService.csou serviço afetado - Este documento —
frontend/elofiel-docs/docs/regras-campanhas.md - Textos de ajuda —
campanha-form.component.ts(painel contextual e hints inline) - Memória do projeto —
C:\Users\elvis\.claude\projects\Z--next-fidelidade\memory\ - Testes de integração — backend
*.Testse cenários de ponta a ponta - OpenAPI spec —
frontend/elofiel-docs/openapi/(exemplos de request/response)