Pular para conteúdo

Regras de Negócio

🎯 Propósito: este é o documento de consulta do suporte interno. Reúne as regras que governam o sistema "por baixo dos panos" — justamente as que a gente esquece em features pouco usadas. Quando alguém perguntar "por que o sistema fez X?", a resposta provavelmente está aqui.


1. Pessoas, Usuários e Perfis de acesso

  • Usuário ≠ Pessoa. Usuário faz login (e-mail/senha); Pessoa salta/trabalha e tem carteira. Um usuário pode estar ligado a uma pessoa, mas não precisa.
  • Perfil de acesso ≠ classificação da pessoa. Os perfis de acesso (o que o usuário pode fazer) são um conjunto fechado: Administrador, Operacional, Financeiro, Staff de campo, Dobrador, Atleta. A classificação operacional da pessoa (instrutor, piloto, tandem, videomaker, rigger…) vive nas flags da Pessoa, derivadas de licença, e serve para filtrar/escalar — não dá acesso. Acesso é validado por capability (ação): GET exige *.view, alteração exige *.manage/ação específica. O mapa role→capability é editável pelo system admin em /system/rbac (vale para todas as DZs); o código é o seed + fallback, e o perfil Administrador sempre tem tudo (não dá para se trancar fora).
  • Modo "somente leitura" automático nas telas. Quando o usuário tem o *.view mas não o *.manage de uma área (ex.: financial em Locais, ou staff no Manifesto), a UI esconde/desabilita as ações de gerência: o manifesto entra em modo travado (sem arrastar, sem check-in), Reservas esconde criar/editar/sync, Pessoas esconde criar/editar/convite/excluir, Configurações da DZ desabilita o form e mostra um selo "Somente leitura", e cada catálogo esconde criar/editar/excluir. Assim o usuário não recebe 403 ao clicar em algo que o perfil não pode.
  • Comparador de perfis na tela de Equipe. Ao convidar/editar um membro, o ícone ao lado de "Funções" abre um diálogo com a matriz role × área (Gerencia/Visualiza/Aprova) — é a fonte rápida pro admin entender o que vai conceder.
  • Adicionar um Dobrador cria/vincula uma Pessoa automaticamente e marca a flag é_dobrador. Os demais perfis de acesso não marcam flags — a classificação vem das licenças.
  • Só Administrador concede o perfil Administrador. Operacional gerencia a equipe (criar/editar/resetar senha) mas não pode atribuir/alterar/remover o perfil Administrador (sem autopromoção).
  • As funções da Pessoa são derivadas das licenças, não marcadas à mão. é_atleta, é_instrutor, é_rigger, é_piloto, é_rta são atualizadas automaticamente quando a licença/habilitação muda:
  • é_atleta ← licença de salto válida (A/B/C/D/AI)
  • é_instrutor ← habilitação de instrutor válida (TP/AFF/BBF)
  • é_rigger ← certificação de dobra de reserva
  • é_piloto ← habilitação de piloto válida (PP/PC/PLA)
  • é_default_rta: no máximo uma pessoa por DZ pode ser o RTA padrão (auto-selecionado ao criar decolagem).

Validações no cadastro/edição de Pessoa

  • E-mail obrigatório. Toda Pessoa precisa de e-mail; o sistema rejeita criação sem ele.
  • E-mail único por DZ. Não dá pra criar duas Pessoas com o mesmo e-mail no mesmo tenant (constraint unique_people_email_per_tenant). Conflito retorna 409.
  • Documento único por DZ. Mesmo CPF/Passaporte não pode aparecer em duas Pessoas (unique_person_document por tenant_id + tipo + número).
  • CPF é validado. Quando o tipo de documento é CPF, o número passa por validação de dígitos verificadores. Acentos/máscara são strip-ados — armazenamos só dígitos.
  • Tipos de documento aceitos: apenas CPF e Passaporte. Pessoas legadas com RG/CNH continuam funcionando, mas não dá pra cadastrar novos.
  • Deduplicação proativa por nome+nascimento. No fluxo de criação manual, antes de gravar o sistema busca Pessoas com mesmo nome (normalizado) e data de nascimento compatível; se houver candidatos, abre um diálogo de confirmação. O fluxo de lookup CBPq já tem dedup próprio por nome.
  • Normalização automática. Nome e apelido entram com trim + colapso de espaços; e-mail entra em lowercase. Evita duplicatas por digitação inconsistente.

2. Acesso aos Portais

  • Portal do atleta tem dois interruptores em série: o da DZ (portal_enabled) e o da pessoa (portal_access_enabled). Ambos precisam estar ligados.
  • O toggle individual da pessoa (portal_access_enabled) é o que efetivamente dirige o papel de atleta + auto-cadastro/OAuth/vínculo no login. (O antigo toggle por equipe foi removido.)
  • Modo de registro (invite_only vs aberto) decide se o atleta entra só por convite ou por auto-cadastro.
  • Portal do dobrador exige packer_portal_enabled na DZ e a função de dobrador na pessoa.

3. Decolagem (Load) — Máquina de Estados

Transições válidas (qualquer outra é rejeitada):

rascunho   → agendada, cancelada
agendada   → embarque, rascunho, cancelada
embarque   → decolou, agendada, cancelada
decolou    → pousou
pousou     → (final, sem transição)
cancelada  → (final, sem transição)
  • Agendar exige aeronave + piloto licenciado. Sem piloto com habilitação válida, não passa de rascunho.
  • Não dá pra alocar mais slots que a capacidade da aeronave.
  • pousou é o gatilho da cobrança. Veja seção 5.
  • Modo de edição permite editar uma decolagem já pousada (exceção controlada).

4. Manifesto

  • Um manifesto por dia + local. GetOrCreate retorna o existente ou cria.
  • Manifesto pode ser fechado e reaberto.
  • Categoria mínima pode ser definida por manifesto (filtra quem pode saltar).
  • Validação de licença/reserva no manifesto segue o modo configurado na DZ:
  • Estrito → bloqueia.
  • Aviso → alerta e deixa seguir.
  • Não checar (só reserva) → ignora.

5. Cobrança Automática (Billing) — pousou

Quando a decolagem pousa, ProcessLoadBilling(load, data_do_manifesto) roda nesta ordem:

  1. Filtra slots cobráveis — exclui no_show e cancelada.
  2. Carrega produtos + itens de todos os slots.
  3. Resolve preços versionados pela data do manifesto (não a data de hoje).
  4. Slots sem grupo → débito direto na pessoa.
  5. Para cada grupo:
  6. identifica pagantes (paid_by_group = falso) e pagos (paid_by_group = verdadeiro);
  7. cobra cada pagante pelo próprio produto;
  8. rateia o custo de cada slot pago igualmente entre os pagantes (o último absorve o arredondamento);
  9. comissões add-on: produto próprio do staff → credita o staff (itens performer sem tipo de salto);
  10. comissões de pacote: produto do pagante → credita o staff correspondente (itens performer com tipo de salto → acha o slot de staff daquele tipo);
  11. comissões estáticas: credita o destinatário fixo (static_party, rta, agency, equipment).
  12. Repasse de aeronave: se o produto tem repassa_aeronave, credita o dono do avião o preço de vaga vigente, por vaga paga.
  13. Repasse de equipamento: se o slot tem rig, credita o dono do rig via item de equipamento.

Idempotência (não duplica)

Antes de criar, o motor checa transações existentes por tipo de referência: - Cobrança de slot → reference_type = load_billing + slot ID. - Rateio → reference_type = load_billing_split + source_slot_id no metadata. - Comissão → reference_type = load_commission + product_item_id. - Cobrança de complemento → reference_type = load_billing_extra + extra ID.

➡️ Reland / Reprocessar não gera cobrança em dobro.

Extras (complementos) em grupos

  • Extras pertencem a um slot específico, não ao grupo. Cobrados sempre do slot.person_id, mesmo se o slot estiver marcado paid_by_group.
  • Em tandem completo, os extras digitados no diálogo de alocação ficam no slot do passageiro (leader). Antes desse fix, o fluxo de criação de grupo ignorava silenciosamente o array de extras.
  • No resumo do grupo (mesmo em loads finalizadas) e no detalhamento por produto do dia, os extras aparecem como linhas próprias.

6. Itens de Produto e Templates

  • Sem itens, toda a receita vai pra DZ.
  • Comissão performer COM tipo de salto = modelo pacote (acha o staff pelo tipo de salto no grupo).
  • Comissão performer SEM tipo de salto = modelo add-on (o staff já está no slot do produto).
  • Quando o item está ligado a um Template, o Template manda. Os campos recipient_type / amount / jump_type efetivos vêm do Template (resolva via template, não pelos campos do item). Os campos no item ficam só por compatibilidade com linhas legadas.
  • Modos de cálculo do template: valor fixo, por vaga paga, % da receita.
  • calculation_mode do template é imutável. Depois de criado, o backend rejeita a troca (ErrCalculationModeImmutable, HTTP 400). Pra trocar o modo, crie um novo template. Pra trocar o valor, use o endpoint de preços (/prices) que cria uma nova versão vigente — o valor não é atualizado pelo PUT do template.
  • bills_aircraft só é relevante em produtos jump. Outros tipos (rental/service/merchandise) são forçados a false no save — mesmo se o frontend enviar true.
  • Operador no extra é exigido só quando o produto tem item performer dinâmico. Produtos service sem performer dinâmico (taxa de parcelamento, p.ex.) viram receita pura sem precisar de operador.

Licenças

  • Emissor (CBPq/USPA/ABPq/Outro) é separado de Federação: emissor é a confederação que emite a licença; federação é a associação regional (livre texto). Lookup CBPq seta Emissor = CBPq automaticamente.
  • (tenant_id, license_number) é único em people_license (sem duplicar nº de licença na DZ).
  • (tenant_id, cis_number) também é único.

7. Preços Versionados

  • Produtos, templates e preço de vaga de aeronave usam intervalos vigente_de / vigente_até (vigente_até = NULL → ativo).
  • A cobrança usa o preço vigente na data do manifesto. Manifesto retroativo cobra o preço da época.
  • O banco impede sobreposição de períodos.
  • Valores de item definidos direto (sem template) NÃO são versionados — são atualizados no lugar. Como o billing roda no mesmo dia, a transação na carteira guarda o valor real como registro histórico.

8. Carteira e Transações

  • Toda Party (pessoa ou organização) tem uma conta por DZ.
  • Transações são imutáveis: nunca são editadas nem apagadas (hooks bloqueiam update/delete). Para corrigir, use ajuste ou estorno (transação relacionada).
  • Cada transação guarda balance_after, referência (load_slot/depósito/ajuste) e metadata — é o registro de auditoria.

9. Dobra (Packer)

  • Dobra só pode ser criada com o manifesto aberto / no mesmo dia.
  • Uma dobra só pode ser desfeita se ainda NÃO foi paga.
  • Origens: auto (motor, ao pousar/decolar), manual_load (reivindicada num slot), manual_independent (avulsa).
  • Status: pendentepago (atleta pagou) / dz_paid (a DZ banca, veio embutido no produto) / waived (cortesia).
  • O gatilho automático (pousou vs decolou) é configurável na DZ (packer_auto_entry_trigger).
  • Dobrador pode ter preço próprio por serviço (override do preço padrão do catálogo).
  • Pagamento atleta→dobrador passa por confirmação: pendente_confirmaçãoconfirmado / contestado.

10. Equipamento (Rig) e Reserva

  • Validade da reserva = data da dobra + rig_reserve_pack_expire_months (config da DZ, padrão 6).
  • Re-dobra (repack) deixa a reserva pendente de aprovação → precisa ser aprovada.
  • Validação de reserva vencida no manifesto segue manifest_reserve_validation_mode (estrito/aviso/não checar).
  • manifest_allow_without_gear decide se dá pra manifestar sem rig vinculado.

11. Integração Bookeo

  • Mapeie os produtos do Bookeo antes de sincronizar — sem mapeamento, a reserva chega sem produto.
  • A sincronização cria/atualiza reservas e participantes (passageiros viram Pessoas).
  • Importa o depósito pago online.
  • Requer acesso à internet de saída (e o login Google/OIDC também). Em deploy serverless, isso tem implicações de infra — ver CLAUDE.md.

12. Multi-tenant

  • A DZ = Tenant. Todo dado tem tenant_id e é isolado por tenant.
  • As requisições da área administrativa carregam o tenant via header (X-Tenant-ID) e validam o acesso do usuário àquele tenant.

13. Cadastro Público de Atletas

Página pública por DZ (/r/{slug}) onde atletas se cadastram sozinhos. Opt-in — a DZ habilita em Configurações → Dropzone → Portal. Detalhes operacionais em Cadastro Público de Atletas.

Máquina de estados da verificação

Toda Pessoa tem verification_status com 3 valores:

verified              → criada pela DZ ou aprovada após cadastro público (default)
pending_verification  → veio do cadastro público, aguarda DZ revisar
rejected              → DZ rejeitou (motivo registrado, atleta não pode re-tentar)

Transições válidas:

  • DZ-created → verified (sempre, default)
  • Cadastro público + auto_approve OFF → pending_verification
  • Cadastro público + auto_approve ON + CBPq validado → verified
  • Cadastro público + auto_approve ON + CBPq não validado → pending_verification (mais seguro)
  • pending_verificationverified (DZ clicou Verificar)
  • pending_verificationrejected (DZ clicou Rejeitar com motivo)
  • rejected é terminal — não volta nem é retentável pelo formulário

Match (bloqueio de duplicata)

A página pública não atualiza Person existente — bloqueia. As chaves de match:

  1. email (case-insensitive)
  2. cpf (sem máscara)
  3. passport_number
  4. name + birth_date (nome case-insensitive, data exata)

Match contra Persons em qualquer status (verified, pending_verification, rejected). Rejeitado bloqueia — força o atleta a contatar a DZ.

Integração CBPq

Se o license_issuer = "CBPq" e license_number preenchido, o backend faz lookup server-side durante o submit (não no cliente — sem endpoint público de lookup, evita virar proxy de scraping). Resultados:

  • validated → snapshot completo salvo em cbpq_lookup_snapshot (JSONB); ao verificar, cbpqSyncService.Sync() popula licença + endorsements + histórico localmente.
  • not_found ou unavailable → segue para verificação manual; nunca bloqueia o envio.

Conta de portal

A conta no Portal do Atleta não é criada na submissão — só na verificação. Quando a DZ verifica E o setting creates_user está ligado, o User é criado e o email de setup-password é enviado (esse é o "email proof" do endereço — se for fake, ninguém ativa, sem dano colateral).

Anti-fraude

  • reCAPTCHA v3 com threshold ≥ 0.5
  • Rate limit 5 submissões/hora por IP (tabela public_registration_attempts)
  • Audit: self_registered_at, self_registered_ip, self_registered_user_agent gravados na Person

Notas de manutenção (para devs)

Estas não afetam o usuário final, mas evitam pegadinhas no desenvolvimento.

  • PeopleRepository.Update tem whitelist de colunas (Select(...)). Colunas novas na Pessoa não persistem silenciosamente até serem adicionadas à lista. Se um campo novo "não salva", é aqui.
  • O guia técnico do motor de cobrança (com diagramas) está em Motor de Cobrança (anexo técnico).