andré santos

projeto

Plataforma de gestão para empresa de formaturas

Sistema que substituiu planilhas, WhatsApp, Google Drive e papel na operação de uma empresa de formaturas com mais de quinze anos.

Next.jsNestJSPrismaMySQLTailwind CSSPagar.meJWTHTTP-only cookies

contexto

A Charme Eventos é uma empresa de formaturas com mais de quinze anos de operação. Quando começamos a conversar, a operação rodava em quatro lugares ao mesmo tempo: planilhas, pastas de papel, Google Drive e WhatsApp. Nenhuma dessas ferramentas tinha sido escolhida com planejamento, foram entrando ao longo dos anos conforme a necessidade aparecia.

Quando comecei a mapear a operação, os problemas tinham endereço: cerca de R$ 2.500 por ano em lançamentos duplicados porque os comprovantes chegavam pelo WhatsApp sem rastreabilidade, um ciclo de fechamento contábil de cinco dias por causa da entrada em lote, e em média cinco minutos pra encontrar um dado básico como o status de pagamento de um participante.

A primeira pergunta foi se não dava pra adaptar um ERP pronto. O problema é que formatura tem particularidades que ferramentas genéricas não cobrem. Cada evento tem dezenas de pagadores com parcelamentos, contratos e formas de pagamento diferentes. Forçar isso num sistema de prateleira significava treinar a equipe pra trabalhar do jeito que o software espera, em vez do contrário. Já tinha visto o que acontece quando uma equipe pequena tenta se moldar a um sistema feito pra outra coisa. Construímos sob medida.

o que foi construído

Eventos

  • Cadastro e gestão de eventos de formatura
  • Página individual por evento com dados operacionais e checklist
  • Informações gerais, datas e status do evento

Participantes

  • Cadastro de formandos por evento com grupos e turmas
  • Valores esperados por participante
  • Controle de recebimentos vinculados ao participante e ao evento

Financeiro

  • Registro de receitas e despesas por evento
  • Exportação de relatórios em PDF
  • Anexo obrigatório de comprovante por lançamento
  • Integração com pagamentos confirmados pelo webhook

Storage

  • Pastas e arquivos por evento, modelados como árvore auto-referencial no banco
  • Metadados no MySQL, arquivos no disco
  • Thumbnails geradas em background após o upload retornar
  • Links públicos com tokens de 192 bits gerados com randomBytes(24)
  • Revogação não-destrutiva com revokedAt
  • Compartilhamento de pastas com pessoas externas, com permissões configuráveis e prazo de expiração

Permissões

  • Seis módulos: financeiro, cadastros, anexos, cobranças, eventos e usuários
  • Três níveis por módulo: none, view e manage
  • Validação no backend antes de qualquer leitura ou alteração
  • Interface oculta seções que o usuário não pode acessar, mas o backend não depende disso

Pagamentos

  • Link público de cobrança com token próprio
  • Frontend tokeniza o cartão direto na API do Pagar.me antes de qualquer requisição chegar ao backend
  • Backend recebe apenas o card_token, nunca dados brutos do cartão
  • Transação financeira criada somente após confirmação do webhook
  • Constraint única no banco em (sourceType, sourceId) contra duplicidades
  • Handler de webhook idempotente com verificação de status antes de escrever

Auditoria

  • Registro de módulo, ação, tipo de entidade e ID
  • actorType diferencia usuário autenticado, link público e sistema
  • beforeData e afterData em JSON para alterações sensíveis

decisões técnicas

Autenticação com JWT em cookie HTTP-only

O token fica num cookie com httpOnly: true e sameSite: none. Ele não toca o JavaScript do frontend em nenhum momento. A estratégia JWT no NestJS lê o cookie diretamente da requisição.

Permissões por módulo com três níveis

Cada usuário tem um registro de permissão com seis módulos: financeiro, cadastros, anexos, cobranças, eventos e usuários. Cada módulo aceita none, view ou manage. A interface esconde o que o usuário não pode ver, mas o backend valida antes de qualquer operação. Ocultar na tela não é suficiente.

Storage como árvore auto-referencial

A equipe estava acostumada com o Google Drive, então a interface foi pensada pra se parecer com ele: thumbnails, seleção arrastando, mover entre pastas. A curva de aprendizado foi quase zero. No banco, pastas e arquivos ficam na mesma tabela com um parentId. O registro entra no banco primeiro. Se o disco falhar, o registro é deletado no catch. Assim o banco nunca tem referência órfã.

Links públicos com tokens de 192 bits

Os tokens são gerados com randomBytes(24), não IDs sequenciais. Revogar marca revokedAt mas não apaga o registro. O histórico de quem gerou, quando e quando revogou fica preservado. Quando alguém navega dentro de uma pasta compartilhada, o sistema percorre a árvore pra confirmar que o destino ainda está dentro dos limites do link. Não confiar só nos parâmetros da requisição é o que evita que alguém com um link de uma pasta consiga acessar uma pasta irmã manipulando o parentId na URL.

Tokenização de cartão no frontend

O frontend envia os dados do cartão direto pra API do Pagar.me e recebe um card_token. O backend só recebe esse token, nunca os dados brutos. A transação financeira é criada só dentro do handler do webhook, depois que o gateway confirma o pagamento. A cobrança só vira movimentação financeira depois da confirmação real do gateway.

Webhook idempotente com constraint no banco

O handler verifica o status atual antes de escrever qualquer coisa. Se a cobrança já está como paga, retorna sem criar nada. O Pagar.me pode reenviar o mesmo webhook mais de uma vez por perda de conexão ou retry automático. Sem essa verificação, o sistema criaria dois lançamentos pra um pagamento só. O banco ainda tem uma constraint única em (sourceType, sourceId) como segunda linha de defesa.

SHA-256 em comprovantes financeiros

A duplicação era um dos vazamentos mais caros e também um dos mais difíceis de auditar. Quando o comprovante era impresso e digitado manualmente, não tinha como saber depois se aquele mesmo comprovante já tinha entrado antes. A solução foi calcular o hash SHA-256 sobre o buffer no momento do upload e comparar com os já cadastrados. Se for igual, rejeita antes de chegar ao banco ou ao disco. É instantâneo e elimina o caso mais comum sem depender da atenção do usuário.

capturas de tela

Interface do módulo de storage com estrutura de pastas e thumbnails

Módulo de storage com estrutura de pastas e thumbnails das fotos do evento

Modal de compartilhamento de pasta com configurações de permissão

Modal de compartilhamento com opções configuráveis de expiração, download e subpastas

Sistema bloqueando comprovante duplicado

Sistema rejeitando um comprovante já cadastrado em outro lançamento

Lista de cobranças com link de pagamento

Lista de cobranças com status em tempo real e link copiável para envio via WhatsApp

o que aprendi

A coisa mais importante que entendi nesse projeto é que digitalizar uma empresa pequena tem menos a ver com código e mais com adoção. Quando o sistema obrigava o usuário a sair pra usar outra ferramenta, ele voltava pra planilha.

A maior parte do valor que entreguei não foi nas partes técnicas mais interessantes. Foi em decisões de interface: onde colocar um botão, qual campo deixar obrigatório, como agrupar a lista de lançamentos. Coisas que parecem pequenas mas que fazem alguém preferir usar o sistema em vez da planilha antiga.

Financeiro exige consistência. Um lançamento duplicado é difícil de auditar depois. A solução não foi confiar na atenção de quem usa, foi tornar fisicamente difícil a duplicata entrar.

Plataforma de gestão para empresa de formaturas - André Luiz