Pular para o conteúdo principal

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 ativaAceito?
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."

Regra universal e não configurável

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)

CampoSalvo no bancoAplicado 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 campanhaCampo utilizadoCálculo
CashbackcashbackPorCompraValor fixo em R$ por venda
PontospontosPorCompraQuantidade 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 brutoResultado
R$ 1,239R$ 1,23
R$ 7,505R$ 7,50
R$ 0,999R$ 0,99

Arredondar

Math.Round(valor, 2, MidpointRounding.AwayFromZero)
Valor brutoResultado
R$ 1,235R$ 1,24
R$ 7,505R$ 7,51
R$ 0,994R$ 0,99

Regra de negócio: Truncar favorece 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).

TipoCálculo da expiração
DiasdataExpiracao = dataEmissao + dias
DataFixadataExpiracao = dataFixaConfigurada (igual para todos os lotes)
FimMesdataExpiracao = ú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

TipoComportamento
DescontoValorDesconto em R$ direto na venda
DescontoPercentualPercentual sobre o valor da venda
BrindeRecompensa 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

permiteUsoParcialComportamento
true (padrão)Cliente pode usar qualquer parte do saldo
falseO 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:

  • BackendVendaService.cs, ResgateService.cs ou serviço afetado
  • Este documentofrontend/elofiel-docs/docs/regras-campanhas.md
  • Textos de ajudacampanha-form.component.ts (painel contextual e hints inline)
  • Memória do projetoC:\Users\elvis\.claude\projects\Z--next-fidelidade\memory\
  • Testes de integração — backend *.Tests e cenários de ponta a ponta
  • OpenAPI specfrontend/elofiel-docs/openapi/ (exemplos de request/response)