← Início
EM PRODUÇÃO Novembro 2025 – Atual (ref. 29/07/2026, ~8 meses)

Barber Cashflow

Produto próprio: desenvolvido, implantado e mantido integralmente por Paulo.

NÚMEROS
+R$ 245 mil volume transacionado pelo sistema 25/11/2025 – 29/07/2026 · ~8 meses
~7.000 vendas registradas no período
5 barbeiros operando diariamente
+4.000 linhas de testes automatizados
STACK
Django Django REST Framework JWT PostgreSQL Docker Nginx Cloudflare PWA OpenAI Embeddings (text-embedding-3-small) FAISS
O PROBLEMA

A barbearia registrava agendamentos, vendas, comissões e caixa de forma manual e dispersa. Sem visibilidade consolidada, fechar o dia exigia reconciliar anotações de fontes diferentes. O pedido inicial era um sistema de agendamento.

No meio do desenvolvimento veio uma virada: a arquitetura comportava mais de uma barbearia. O sistema virou SaaS. Essa decisão trouxe, de uma vez, isolamento de dados entre tenants, responsabilidade sobre LGPD e a necessidade de subir atualizações sem interromper uma operação que já rodava com cliente real.

O sistema hoje registra vendas por forma de pagamento (PIX, cartão, dinheiro), calcula comissão por barbeiro por venda, controla estoque e fecha o caixa com conciliação automática. Funciona como PWA: instalável no celular do barbeiro, com operação offline parcial.

ARQUITETURA
Isolamento multi-tenant: requisição → middleware → filtro por tenant → PostgreSQL

Django + DRF servindo a API REST com autenticação JWT; PostgreSQL como banco compartilhado entre tenants; isolamento por tenant_id resolvido via TenantMiddleware a cada request; Nginx como reverse proxy; Docker para build reproduzível; Cloudflare para SSL e CDN. A camada RAG corre separada: dados do sistema são chunked, convertidos em embeddings com a API da OpenAI (text-embedding-3-small) e indexados com FAISS. PWA com service worker para uso offline parcial.

DECISÕES
DECISÃO

Multi-tenancy: shared-db com tenant_id

Problema
Múltiplas barbearias precisam de isolamento total de dados. Um barbeiro de um salão não pode ver, nem acessar por erro, os dados de outro.
Alternativas
Banco separado por tenant: isolamento máximo, mas custo operacional alto e migrations em N bancos a cada mudança de schema. Schema separado no PostgreSQL: migrations multiplicadas e roteamento de conexão complexo.
Escolha
Base compartilhada com tenant_id em cada tabela relevante. TenantMiddleware resolve o tenant atual a partir de três fontes em ordem de prioridade: subdomínio → cabeçalho HTTP → parâmetro de query. O filtro por tenant é obrigatório em toda queryset exposta.
Resultado
Um tenant pagante em produção. A arquitetura suporta múltiplas barbearias na mesma instância Django e no mesmo banco PostgreSQL, e adicionar um novo tenant não requer redeploy nem nova instância de banco. O isolamento entre eles é garantido por filtro obrigatório em queryset e provado por teste, não por volume de operação.
DECISÃO

Testes de isolamento retornam 404, não 403

Problema
A suíte de testes precisa provar que um tenant não acessa dados de outro. A resposta de erro importa tanto quanto a negação de acesso em si.
Alternativas
403 (Forbidden): semanticamente correto, já que o recurso existe mas o acesso é negado. 404 (Not Found): não revela se o recurso existe ou não.
Escolha
404. A view trata o recurso de outro tenant como inexistente para o tenant solicitante. Os testes de isolamento tentam ativamente acessar dados de outro tenant e verificam que a resposta é 404, e não 403.
Resultado
Isolamento validado por testes automatizados. Um atacante com credenciais válidas de um tenant não consegue nem confirmar a existência de recursos de outros tenants.
DECISÃO

transaction.atomic no cancelamento de venda

Problema
Cancelar uma venda toca três domínios simultaneamente: estoque (devolução de produto), comissão do barbeiro (estorno proporcional) e caixa (lançamento de cancelamento). Uma exceção no meio da operação deixaria os três em estados inconsistentes entre si.
Escolha
transaction.atomic() envolve toda a operação de cancelamento. Se qualquer etapa falhar (por exceção Python, erro de banco ou violação de constraint), o PostgreSQL reverte todas as alterações automaticamente.
Resultado
Garantia de atomicidade delegada ao banco. A lógica de negócio não precisa implementar rollback manual. Em 8 meses de operação: zero registro de inconsistência financeira por cancelamento parcial.
DECISÃO

Soft delete para preservar histórico financeiro

Problema
Registros financeiros precisam de rastreabilidade completa. Excluir fisicamente uma venda apagaria o histórico contábil e quebraria a conciliação de períodos já fechados.
Escolha
Soft delete com campo is_deleted e SoftDeleteManager que filtra automaticamente registros excluídos em todas as queries padrão. Queries de auditoria e relatórios usam .all_objects para acesso irrestrito ao histórico.
Resultado
O usuário cancela registros sem perder histórico. Relatórios de períodos anteriores permanecem íntegros mesmo após cancelamentos posteriores.
DECISÃO

RAG real para o assistente interno

Problema
Os barbeiros e o dono precisam consultar dados do sistema em linguagem natural: saldo do dia, comissão da semana, produto com estoque baixo. Keyword search não entende variações naturais de pergunta nem contexto financeiro.
Escolha
Pipeline RAG completo: chunking semântico dos dados do sistema → embeddings com OpenAI text-embedding-3-small → índice FAISS para busca vetorial por similaridade → contexto relevante injetado no prompt de resposta.
Resultado
O usuário pergunta em linguagem natural e recebe resposta contextualizada com dados reais do sistema, sem navegar por menus. O retrieval é semântico de verdade, não keyword search com GPT na frente.
O QUE QUEBROU E COMO RESOLVI
● INCIDENTE

N+1 na listagem de vendas

Sintoma
A listagem de 20 vendas demorava visivelmente. O cliente notou a lentidão semanas depois de o sistema entrar em produção. Não havia monitoramento de query count configurado no início.
Investigação
Django Debug Toolbar mostrou 61 queries para renderizar 20 registros: 1 query para listar as vendas + 20 queries para o barbeiro de cada venda + 20 para o serviço + 20 para o agendamento associado.
Causa
As relações FK (barbeiro, serviço, agendamento) não estavam sendo carregadas previamente na queryset da view. O ORM emitia uma query separada para cada objeto no momento em que o template acessava cada FK.
Correção
select_related('barbeiro', 'servico') para as FKs diretas, vira um JOIN único. prefetch_related('agendamento') para a relação inversa, vira uma query IN separada. Resultado: 61 queries → 3 queries. Mesma listagem, sem alteração no template.
RESULTADO

+R$ 245 mil em volume transacionado pelo sistema entre 25/11/2025 e 29/07/2026 (~8 meses), com ~7.000 vendas registradas e 5 barbeiros operando diariamente. Recorte por forma de pagamento: PIX R$ 143.335 · Cartão R$ 72.210 · Dinheiro R$ 30.150.

Nenhum incidente de isolamento em 8 meses com o tenant em produção. O acesso cross-tenant é coberto por testes que tentam ativamente ler dados de outro tenant e verificam o retorno 404. Suíte de +4.000 linhas de testes automatizados cobrindo isolamento multi-tenant, atomicidade de operações financeiras, soft delete e a interface RAG.

CAPTURAS
APRENDIZADO

Foi nesse momento que deixei de enxergar o software apenas como código e passei a enxergá-lo como um produto que precisa continuar funcionando enquanto evolui. A virada de sistema único para SaaS (com um cliente real já usando) tornou concreto o que antes era teórico: isolamento de dados, consistência transacional e testes como documentação viva do comportamento esperado.