Pular para conteúdo

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:

  1. valida usuário, oferta, partner e unidade;
  2. aceita ofertas active|approved e, para campanha, valida estado não terminal;
  3. valida entitlement Premium ou reserva uma recompensa de indicação compatível;
  4. bloqueia outro cupom ativo para a mesma oferta e cooldown de três horas após uso;
  5. reserva uma vaga no limite da oferta;
  6. cria coupon_redemptions com 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.

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