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.
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

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

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

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

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.