Avalia Sales
Um sistema de vendas reconstruído,
com cobrança por gateway de pagamento e identificação das placas por QR Code e NFC.
O Avalia Sales parte de um sistema de vendas em Laravel/PHP. Esta implementação reescreve a base em .NET, React e PostgreSQL, em servidor próprio, com o gateway Pagar.me. É a infraestrutura de uma rede de revendas que vende produtos e serviços pelo celular, com estoque, comissões e financeiro numa única plataforma.
Pedro Fernandes, responsável pelo projeto, desenvolvimento e operação da plataforma, de ponta a ponta.
Em produção em sales.avaliaone.com.br, com o código aberto.
Arquitetura: API e Worker em .NET, telas em React, dados no PostgreSQL
A API responde sem esperar as integrações - o Worker processa filas, eventos de pagamento e e-mails com retentativa.
Navegador
- React
- TypeScript
- TanStack Query
- Tailwind CSS
Aplicação
- ASP.NET Core
- EF Core
- SignalR
- .NET Worker Service
Dados e serviços
- PostgreSQL
- Pagar.me
- Resend
- D4Sign
- Protheus
Atualização em tempo realPostgreSQL (LISTEN/NOTIFY)APISignalRtela do vendedor
- Linux
- Docker Compose
- Nginx
- Let's Encrypt
- GitHub Actions
- Sentry
Antes e depois da atualização
| Camada | Antes | Depois |
|---|---|---|
| Aplicação | Laravel 12, PHP 8.3 | ASP.NET Core e Worker Service em C#, em camadas |
| Telas | Blade, Alpine.js e Tailwind | React e TypeScript, com tipos gerados do contrato OpenAPI |
| Banco de dados | MySQL | PostgreSQL com row-level security por revenda |
| Hospedagem | Hospedagem compartilhada | Servidor próprio com Docker Compose, Nginx e TLS |
| Publicação | Script no servidor, disparado por cron após o push | GitHub Actions com testes, análise do SonarCloud, aprovação manual e health check |
Decisões de engenharia e evidências
Cada uma tem registro (ADR) ou documento no repositório.
-
Revendas isoladas em duas camadas: aplicação e banco de dados
Filtro do EF Core e row-level security no PostgreSQL: se a aplicação falhar, o banco recusa.
Ver contexto e evidência
- Contexto
- Várias revendas usam o mesmo banco, cada uma com clientes, vendas e dinheiro próprios.
- Decisão
- Filtro global do EF Core pela revenda do login e row-level security forçada no PostgreSQL. A aplicação conecta com um papel sem permissão para ignorar a regra.
- Motivo
- Uma consulta que esqueça o filtro não alcança dados de outra revenda: o banco recusa.
- Evidência
- Um teste reprova a publicação se uma tabela com revenda nascer sem a regra.
-
Pagamento confirmado na origem, no gateway, e aplicado uma vez
A venda só fica paga depois de o Worker reconsultar a cobrança no Pagar.me, uma vez por evento.
Ver contexto e evidência
- Contexto
- O webhook do Pagar.me pode chegar repetido, fora de ordem ou forjado, e a rede pode cair no meio da cobrança.
- Decisão
- A cobrança é gravada antes de chamar o Pagar.me, numa transação que recusa uma segunda cobrança viva da mesma venda. O webhook só registra o evento. O Worker reconsulta a cobrança no gateway e trava a linha do evento (FOR UPDATE SKIP LOCKED) antes da baixa. Uma vez por dia reconsulta também as cobranças pagas, para pegar estorno e chargeback feitos no painel do Pagar.me.
- Motivo
- A venda só muda de situação pelo estado confirmado no Pagar.me, e o mesmo aviso não é aplicado duas vezes.
-
Aritmética monetária em inteiros e razão append-only
Valores em centavos como inteiros, sem ponto flutuante, e razão só de inserção: correção entra como estorno.
Ver contexto e evidência
- Contexto
- Split, comissão, estorno parcial e fechamento de mês precisam fechar ao centavo.
- Decisão
- Valores inteiros em centavos. O razão só recebe inserção: correção entra como estorno. O mês fechado é travado por trigger, e o banco nega UPDATE e DELETE no razão ao papel da aplicação.
- Motivo
- Qualquer saldo pode ser refeito a partir das linhas, e nenhuma correção apaga o que já foi lançado.
-
Gravação repetida responde igual, sem efeito duplo
Toda gravação POST autenticada aceita Idempotency-Key: a mesma chave devolve a resposta gravada, sem cobrar nem lançar de novo.
Ver contexto e evidência
- Contexto
- Duplo clique, rede lenta e nova tentativa do navegador reenviam a mesma gravação.
- Decisão
- Um middleware guarda a resposta por 24 horas, por usuário e rota. A mesma chave com outro corpo responde 422, e a execução simultânea responde 409. O front manda uma chave por chamada.
- Motivo
- Repetir o pedido não cobra, não comissiona e não envia e-mail duas vezes.
- Evidência
-
Migração incremental a partir do sistema em Laravel/PHP
Rotas trocadas uma a uma, dados importados numa transação e virada concluída em 24/10/2026, sem mudar o endereço das placas vendidas.
Ver contexto e evidência
- Contexto
- O sistema anterior estava em uso, com placas impressas cujo endereço do QR Code não pode mudar.
- Decisão
- As rotas do sistema anterior passaram a redirecionar para a nova plataforma uma a uma (padrão strangler fig). Os dados foram convertidos a partir do backup e importados numa transação única, depois de um ensaio. As placas estão hoje na plataforma, e o endereço antigo (/q/) repassa para o novo com 302, sem cache. O histórico financeiro das vendas também é importável, para a DRE dos meses de origem.
- Motivo
- A operação continuou durante a troca, e uma importação com erro é desfeita por inteiro.
- Evidência
A aplicação em funcionamento: uma venda em seis etapas
No celular do vendedor, com leitura do QR Code da placa e cobrança pelo Pagar.me. As etapas 3 a 6 aplicam as decisões de engenharia acima.
Nova venda
- Placa de avaliaçãoQR Code e NFCR$ 120,00
- Perfil no GoogleServiço da operadoraR$ 180,00
TotalR$ 300,00
Cobrar R$ 300,00Ler a placa
Placa AV7K2Q vinculada
Cobrança
PixR$ 300,00
Aguardando pagamento
Cobrança
Pago
Conferido no Pagar.me
Divisão
- Revenda
- R$ 234,00
- Operadora
- R$ 36,00
- Comissão
- R$ 30,00
Lançado no razão
Placa no ar
Padaria Exemplo
Avaliação no Google
Conte como foi a sua experiência
PublicarPasso 1 de 6
Simulação com valores de exemplo.
-
Uma cobrança para a venda inteira. Cancelar um item devolve só a parte dele.
-
BarcodeDetector nativo, com fallback em WebAssembly.
-
A cobrança é gravada antes de chamar o Pagar.me, e o cartão é tokenizado no navegador.
-
O webhook do Pagar.me é gravado e conferido na origem. A tela recebe por SignalR.
-
Valores em centavos no razão, split do Pagar.me por recebedor e comissão paga em repasse. Caixa e DRE saem do razão, com a receita da placa reconhecida em 12 partes.
-
Redirecionamento configurável sem reimprimir a placa - gravação NFC pelo Web NFC.
Quality Assurance
Verificações do repositório e da produção a cada publicação. A contagem de testes indica os casos executados, não a cobertura.
-
Testes automáticos
519 testes da API (xUnit, contra PostgreSQL real, com o papel e o RLS da produção), 223 do front (Vitest, com axe) e 25 cenários ponta a ponta (Playwright), no celular e no computador.
Testes da API -
Publicação controlada
CI verde, com análise do SonarCloud, aprovação manual, cópia do banco antes de subir e health check pelo domínio. Rollback por nova publicação do commit anterior.
Fluxo de publicação -
Contrato entre front e API
Tipos do front gerados do OpenAPI da API. O CI falha se estiverem desatualizados.
Verificações do CI -
Erros rastreados por versão
Erros da API, do Worker e do navegador chegam ao Sentry marcados com o commit publicado. Monitor de disponibilidade em /api/health a cada 60 s e check-in do Worker a cada 5 minutos.
-
Segurança e dados pessoais
Revisão em quatro frentes (autorização e isolamento, dinheiro e fila, sessão e entrada, front-end), com testes no repositório. Sessão curta com refresh rotativo, segundo fator, captcha no cadastro público e cartão tokenizado no navegador. Contato de cliente alheio sai mascarado para o vendedor.
-
Cópia do banco fora do servidor
Cópia diária cifrada (AES-256) para um bucket no Cloudflare R2, conferida pelo ETag, com retenção de 30 dias, e também por e-mail, cifrada. Só vale a cópia que o pg_restore consegue ler.
Backup e restauração
Integrações e o estado de cada uma
| Serviço | Uso | Estado |
|---|---|---|
| Pagar.me | Pix, cartão, marketplace com split por recebedor, estorno total ou parcial e webhook com reconsulta | Em produção |
| Resend | E-mails transacionais com fila e retentativa | Em produção |
| Google Places API e Maps API | Localização do estabelecimento e mapa de vendas | Em produção |
| BrasilAPI | Razão social e endereço por CNPJ e CEP | Em produção |
| Gotenberg | Contrato e distrato em PDF, num contêiner sem rede | Em produção |
| Sentry | Erros com a versão publicada, monitor do site e do Worker | Em produção |
| Cloudflare R2 | Cópia diária do banco, cifrada, fora do servidor | Em produção |
| Cloudflare Turnstile | Captcha no cadastro público | Em produção |
| SonarCloud | Análise de código e cobertura dos testes no CI | No CI |
| D4Sign | Assinatura eletrônica do contrato de revenda | Sandbox |
| Protheus | Pedido de placas pago gerado como pedido de venda no ERP | Standby/Endpoint |
Aprofundamento
Documentação versionada com o código, para análise aprofundada.
- Regras do produtoPDD: cada regra de venda, cobrança, comissão e placa
- ArquiteturaSDD: camadas, tabelas, integrações e o registro de mudanças
- 22 registros de decisãoADRs com contexto, opções e consequências
- OperaçãoInstalação, publicação, volta de versão, backup e restauração
- Contrato da APIOpenAPI de onde saem os tipos do front
- Código-fonteRepositório completo no GitHub
Camadas do código e responsabilidades
| Projeto | Responsabilidade |
|---|---|
| Domain | Entidades e regras de negócio, sem dependência de pacote |
| Application | Casos de uso e as interfaces das integrações |
| Infrastructure | EF Core, migrations, clientes HTTP e simuladores |
| Api e Worker | ASP.NET Core e o processamento em segundo plano |
Retentativa automática de HTTP só em métodos seguros, para uma cobrança não ser reenviada por engano.
White label com contraste calculado
Cada revenda aplica logo e cor nas telas. A plataforma escolhe a cor do texto do botão e escurece o destaque até a leitura passar no critério do WCAG.
Botão com texto branco.
Revenda Aurora
VendasClientesPlacas
Padaria SolPaga
Ótica LumeVer ficha
Nova vendaAcesso
Plataforma em operação. Solicite uma demonstração ou implante a partir do código no GitHub.