Ofertas e cupons¶
Feed de ofertas¶
Quando há latitude e longitude, OfferRepositoryImpl.getFeedOffers chama getOfferFeed. A Function exige autenticação e recebe:
{
"userLat": -23.0000,
"userLng": -46.0000,
"radiusKm": 30,
"limit": 30,
"categoryFilter": "categoria-opcional",
"filterIds": ["filtro-opcional"],
"searchText": "texto-opcional",
"excludeOfferIds": ["oferta-ja-exibida"]
}
Ela considera ofertas regulares active|approved, unidades/partners válidos, disponibilidade do limite e raio geográfico. O score base recebe boosts por proximidade, desconto, tier Premium e campanha, seguido por seleção ponderada com penalidade de diversidade. A resposta preserva a ordem calculada:
{
"algorithmVersion": "geo_weighted_balanced_v1",
"generatedAt": "2026-01-01T12:00:00.000Z",
"items": [
{
"offerId": "offer-example",
"restaurantId": "store-example",
"partnerId": "partner-example",
"distanceKm": 1.25,
"score": 145,
"algorithmVersion": "geo_weighted_balanced_v1",
"rankingMetadata": {}
}
]
}
Sem coordenadas, o adapter Dart usa consulta Firestore de fallback. Essa diferença deve ser considerada ao testar ordenação.
Curadoria e autoria¶
Parceiros podem criar/editar apenas campos permitidos pelas Rules. Curadoria e autoria sensível ficam no backend:
| Operação | Function | Garantia |
|---|---|---|
| Aprovar, rejeitar, pedir ajuste, mudar tier | curateOffer |
exige super_admin, grava curador e timestamp |
| Criar em nome do parceiro | createOfferAsAdmin |
valida partner/unidade e registra autoria administrativa |
| Excluir oferta | deleteOffer |
valida ator e grava deletion_context antes do delete |
| Auditar mudanças | onOfferWritten |
trigger com auth context grava offers/{id}/audit_logs |
Writes diretos em audit_logs, admin_notes e deletion_context são negados. Delete direto de offers também é negado.
Geração e ciclo de vida do cupom¶
stateDiagram-v2
[*] --> active: createCouponRedemption
active --> waiting_checkin: activateCoupon
waiting_checkin --> active: cancelCheckin
active --> cancelled: cancelCoupon
active --> used: markCouponAsUsed
waiting_checkin --> used: confirmCheckin / markCouponAsUsed
active --> expired: expireCoupons
waiting_checkin --> expired: expireCoupons
createCouponRedemption recebe apenas offerId; todo o restante é derivado da autenticação e da oferta. Dentro de uma transação, ela:
- valida usuário, oferta, partner e unidade;
- aceita ofertas
active|approvede, para campanha, valida estado não terminal; - valida entitlement Premium ou reserva uma recompensa de indicação compatível;
- bloqueia outro cupom ativo para a mesma oferta e cooldown de três horas após uso;
- reserva uma vaga no limite da oferta;
- cria
coupon_redemptionscom TTL de 24 horas e consome a recompensa, quando usada.
O limite é independente por documento de oferta/unidade. Cancelamento ou expiração libera a reserva. Confirmação converte a reserva em uso sem ultrapassar couponUsageLimit.
Check-in do cliente e do garçom¶
activateCoupon verifica dono, status e validade do cupom, então move active → waiting_checkin. confirmCheckin exige uma sessão de garçom ativa, o garçom incluído na sessão e a mesma unidade do cupom; depois move para used e incrementa os contadores da oferta.
Limite atual da validação geográfica
As telas atuais obtêm uma posição GPS antes de chamar activateCoupon, mas não enviam coordenadas à Function. A callable activateCoupon não recalcula distância. Existe uma callable legada validateGeofence, baseada na coleção coupons e raio de 200 m, enquanto o fluxo principal usa coupon_redemptions; ela não é chamada pelos adapters atuais. Portanto, o código versionado não oferece uma garantia server-side de proximidade no fluxo principal.
Expiração e trending¶
expireCoupons é disparada a cada minuto e sujeita ao throttle operacional (default 30 minutos). Ela busca até 500 cupons vencidos nos estados ativos, processa com concorrência 10 e libera reservas transacionalmente. Falhas individuais são logadas sem impedir o restante do lote.
confirmCheckin incrementa redemptionCount24h e marca isTrending a partir de 10 usos. resetTrendingProducts zera esses campos diariamente às 00:05; esse job não usa o throttle configurável.
Diagnóstico¶
| Sintoma | Verificar |
|---|---|
| Feed vazio com GPS | Auth, localização da unidade, raio, status/origem e limite esgotado |
resource-exhausted ao gerar |
couponUsageLimit, couponReservedCount, couponUsedCount |
| Cupom bloqueado | outro cupom ativo, cooldown de 3h, campanha terminal ou entitlement Premium |
| Garçom não confirma | expiração de waiter_sessions, waiterIds, storeIds, unidade do cupom e status |
| Reserva não liberada | execução do scheduler, índice por status/expiresAt e logs do lote |