← 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 — 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 recebem 404 — 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. Não é keyword search com GPT na frente — é retrieval semântico de verdade.
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 JOIN único. prefetch_related('agendamento') para a relação inversa — vira 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.