VouParticipar · Backend · Análise técnica

Lotes v2 — Levantamento de riscos e decisões

Reforma dos lotes de inscrição: lotes gerais seguidos pelas categorias, overrides por categoria, períodos de datas, limites por lote e histórico do lote por participante. Abaixo: o que já existe no código, os riscos mapeados e as perguntas que destravam o modelo.

Base: código em dev (d0ab218) Ata da reunião 10/10 12/10/2026

Resumo em 30 segundos

Base já existe — mas incompatível

Hoje o lote é um acréscimo (% ou fixo) sobre o preço base, com modo exclusivo (General ou PerCategory), limite e data final — sem data de início. Não há "geral seguido por categoria" nem override.

6

Decisões destravam o modelo

Semântica de preço, auto-avanço na virada, precedência geral × categoria, pendente segurando vaga, fuso horário e o que o site mostra. As respostas definem as tabelas e a lógica de resolução.

Top riscos encontrados

Fuso (UTC vs Brasília) na virada · pendente segura vaga do lote · 1 lote por participante (multi-categoria) · sem GET de lotes (e preço base no site diverge do cobrado) · edits do admin sem guardrails.

Como está hoje

Resumo do estado real do código (não da intenção):

PeçaComo está
PriceBatchAcréscimo (% ou fixo) sobre o preço base da categoria — "lote nunca dá desconto". Campos: nome, %/fixo, ParticipantLimit, EndDate, DisplayOrder, IsActive. Sem data de início.
ModosBatchPriceMode: None / General / PerCategory — exclusivos: ou lote geral, ou por categoria; nunca convivendo.
ResoluçãoPrimeiro lote ativo e não esgotado por DisplayOrder; sem vigente → 409 Event.BatchSoldOut; sem lotes configurados → preço base.
SnapshotParticipante guarda BatchId + BatchName — um só por participante ("primeiro lote com ajuste" entre as categorias).
DeleçãoBloqueia por vendas (pendente conta; cancelado some da contagem). Sem regra de "só futuro".
API adminPUT /batches/settings, POST, PUT, DELETE — não existe GET/listagem (nem usados/restantes/vigente).
PúblicoShowBatchesPublicly é flag morta (nunca lida); GET /events/{id} mostra o preço base — diverge do cobrado no lote.
CancelamentoO registro de cancelado não guarda o lote.
ConcorrênciaCompra pega advisory lock do evento; edição/deleção de lote não pega lock nenhum.

Decisões da reunião × realidade do código

Onde o requisito já tem base e onde o modelo atual não suporta:

Decisão da reuniãoHojeSituação
Lote geral é seguido pelas categoriasModo é XOR — geral e categoria nunca coexistemNão suporta
Categoria pode ter lote próprio que sobrepõe o geralNão existe cadeia de precedênciaNão suporta
Lotes com período (datas) + limite próprioTem EndDate + limite; sem StartDate; sem validação de sobreposiçãoParcial
Último lote sem data/limite rígido"Sem dados" funciona, mas não há noção de "último" nem corte explícitoParcial
Salvar nome e tipo do lote no participanteSalva só nome; tipo não existe; 1 lote por participante (errado p/ multi-categoria)Parcial
Registrar o lote no cancelamentoRegistro de cancelado não tem campos de loteNão existe
Excluir apenas lotes futurosRegra atual é "sem vendas"; sem conceito de futuro sem StartDateNão existe
Desativar lote "sem utilidade"IsActive existe e interfere na resoluçãoExiste a mais
Endpoints para resgatar dadosSem GET de lotes; sem usados/restantes/statusNão existe
Interface agrupada na edição do eventoBackend sem listagem para a tela montarBloqueado

Riscos mapeados

Ordenados por severidade — cada um ancorado no código atual:

Críticopodem custar dinheiro ou gerar dado errado

1. Semântica de preço indefinida

Hoje o lote é acréscimo sobre a base; a reunião fala em "preço de cada categoria por lote". Se for valor final, muda modelo, UI, voucher, relatórios e contrato do front. Decidir antes de codar.

PriceBatch.cs — EffectiveIncreaseBatchPricing.cs

2. Fuso horário na virada

Comparações usam DateTime.UtcNow. Se o front/planilha mandar "23:59" sem offset, o lote vira 3h mais cedo (21h BRT). Precisa regra explícita de fuso na API e na UI.

PriceBatch.IsExhausted

3. Um lote por participante (multi-categoria)

SetBatch grava o primeiro lote resolvido entre as categorias. Com lote específico por categoria, o dado de auditoria/relatório fica errado por design. O correto é guardar por categoria do participante.

Participant.cs — SetBatchCreateInscriptionCommand.cs:314

4. Pendente segura vaga do lote

Carrinho não pago conta no limite; a expiração roda a cada 10 min (60 min em prod) → lote esgota "sem dinheiro". Quando expira/cancela, a vaga volta e o lote mais barato pode reabrir para quem chegar depois (preço regride).

InscriptionRepository.GetBatchOccupancyAsyncPaymentExpirationJob

5. Admin trocando categoria ignora o lote

Re-precifica pelo preço base e não limpa BatchName → cobra/credita errado e o Excel mostra lote antigo com preço novo. Limitação já conhecida e documentada.

AdminUpdateParticipantCommandHandlerParticipant.UpdateCategories

6. Preço base editável com inscrições pagas

UpdateEventCommand passa hasPaidInscriptions: false e dribla o guard do domínio; como o lote é % sobre a base, isso muda o preço das próximas vendas de um lote vigente silenciosamente.

UpdateEventCommand.cs:253Category.UpdatePrice
Altooperação e contratos

7. Corrida cotação → checkout (sem reserva)

Usuário vê "Lote 1 R$100", paga R$120 ou recebe 409 genérico. Com virada por data/limite isso será frequente — precisa UX e erro amigáveis ("lote esgotou, preço atualizado para X").

FilterCategoriesByAgeQueryBatchPricing.Resolve

8. Sem GET de lotes + preço "mentiroso" no site

A tela administrativa prometida não tem de onde ler; e GET /events/{id} devolve o preço base enquanto a cobrança usa o lote. ShowBatchesPublicly nunca é lida.

EventBatchesController (sem GET)EventMappingExtensions.cs:19

9. Edição/deleção de lote sem lock

A compra tranca o evento (advisory lock); o admin não. Editar/criar/deletar em paralelo com vendas = lost update/estado inconsistente.

EventBatchCommandsAcquireEventInscriptionLockAsync

10. "Só futuros deletáveis" sem StartDate

Não há definição de "futuro" hoje. E como cancelado some da contagem, um lote pode voltar a ser deletável depois que todos cancelarem.

DeletePriceBatchCommandHandler

11. UnitOfWork commita Result de falha

O behavior de transação só dá rollback em exceção — validações novas de lote não podem mutar estado antes de retornar erro (ex.: contadores/trackers).

UnitOfWorkBehavior.cs
Médioqualidade e consistência

12. Três comportamentos para "esgotado"

A cotação bloqueia (blocked), o preview de voucher cai para o preço base silenciosamente e a criação devolve 409. Padronizar.

VoucherQueries.cs:99InscriptionQueries.cs:575

13. Pool só de lotes inativos vira 409

RequiresBatch conta inativos como "configurados"; não existe erro NoBatch distinto de sold-out.

Event.RequiresBatchBatchErrors

14. batch_id sem FK + cascade de categoria

Deletar categoria cascateia lotes e pode deixar ids órfãos no participante.

ParticipantConfiguration.cs:67

15. Cobertura de testes fina nos caminhos que vão mudar

Sem testes para lote no add participants, troca de modo, visibilidade pública e regra "só futuros"; vários testes atuais vão quebrar por design.

PriceBatchIntegrationTestsCreateInscriptionBatchTests

16. Documentação quase vazia

Só 2 linhas no wiki sobre lotes; zero no book/README. A regra do repo exige docs na mesma entrega.

docs/wiki/Fluxos-Principais.md:187

Perguntas para o Jonathan

As destrava o modelo são as que precisam de resposta antes de qualquer código — elas definem tabelas e a lógica de virada.

A. Preço e virada

  1. O preço do lote é o valor final da categoria naquele lote ou um acréscimo sobre o preço base (como hoje)?destrava o modelo
  2. Lote esgota antes da data: o próximo abre na hora com o preço novo, ou a venda bloqueia até a data dele?destrava o modelo
  3. Quando o prazo acaba sem esgotar, vira pro próximo — confirmado? Pode existir dia sem lote vigente (venda fechada com aviso)?
  4. Último lote (sem data/limite) fecha quando esgotar o limite da categoria/evento e no encerramento das inscrições (início do evento)?destrava o modelo
  5. Lote específico da categoria sobrepõe o geral em tudo (preço+datas+limite) ou só no preço? Se o específico termina/esgota, a categoria volta pro geral vigente?destrava o modelo
  6. Categoria pode ter mais de um lote específico (A, depois B)? Períodos podem sobrepor? Se sim, qual vale?

B. Datas e fuso

  1. Datas com hora específica ou só dia inteiro? Qual fuso — sempre horário de Brasília? (Hoje o sistema compara em UTC e isso vira lote 3h mais cedo.)destrava o modelo
  2. Precisa agendar lote com data de início futura (abre sozinho dia X), ou a virada é sempre por esgotamento/prazo do anterior?

C. Limites e vagas

  1. Limite do lote conta pagas + pendentes (carrinho segura vaga)? Se o pendente expira, aceita que a vaga volte e o lote "reabra" mais barato?destrava o modelo
  2. Limite do lote geral vale pro somatório de todas as categorias? Como interagem limite do geral × da categoria × do evento (qual manda)?
  3. Grupo (dupla/quarteto) consome N vagas do lote — ok? E se restarem 3 vagas pra um quarteto: fecha a compra (hoje) ou libera?
  4. Cancelamento devolve a vaga pro lote original (pode reabrir lote antigo mais barato) ou some pro lote atual?

D. Histórico

  1. "Tipo do lote" = geral vs categoria ou percentual vs fixo? Qual aparece no relatório?
  2. Participante com várias categorias/lotes diferentes: guardar o lote por categoria (correto) — confirma que é essa a expectativa?
  3. Cancelamento precisa de histórico auditável (lote, valor, motivo, data)? Deve aparecer em algum relatório/Excel?

E. Edição e administração

  1. O que dá pra editar com vendas: preço (recomendo bloquear), datas, limite (aumentar? reduzir?)? Hoje tudo é editável, inclusive preço.
  2. Excluir lote futuro: e se for o único da categoria? E o lote agendado (ainda não começou) pode excluir?
  3. Ordem/sequência: reordenar lotes com vendas? A ordem ou a data define a virada?
  4. Site público: mostra tabela de lotes (preços/datas), só o preço vigente, ou "próximo lote abre em X"?destrava o modelo

F. Comunicação

  1. E-mail/WhatsApp de confirmação mostram o nome do lote? Relatório de vendas por lote é desejado?
  2. Se o preço mudar entre ver e pagar (virada), o front deve mostrar "lote esgotou, preço atualizado — confirmar?" (recomendo) em vez de cobrar diferente sem aviso?

G. Legado

  1. Evento sem lotes continua no preço base e o organizador liga lotes quando quiser (opt-in)?
  2. Inscrições antigas sem lote: coluna "Lote" vazia/"—" nos relatórios, ok?

Dica: dá pra responder direto neste artefato no canal com o Jonathan — as marcadas com destrava o modelo são as únicas bloqueantes para começar o desenho técnico.

Encaminhamento

1. Fechar com o Jonathan (bloqueante)

  • Semântica de preço (A1)
  • Auto-avanço da virada (A2, A4)
  • Precedência geral × categoria (A5)
  • Pendente segura vaga (C9)
  • Fuso das datas (B7)
  • O que o site mostra (E19)

2. Desenho técnico (após as respostas)

  • PriceBatch v2: StartDate/EndDate, precedência geral→categoria, tipo no snapshot
  • Snapshot por categoria do participante (substitui o "1 lote por participante")
  • Occupancy por lote + regras de vaga (pendente/cancelamento/expiração)
  • GET de lotes + DTOs (admin e público) e contrato do front
  • Migração de dados (legado sem lote) e backfill
  • AdminUpdate* lote-aware + padronização do erro de esgotado
  • Matriz de testes: limites, virada (data/limite), concorrência, cancelamento, relatórios