O Quotery está no ar em https://www.quotery.io. É um SaaS B2B multi-tenant que pega a pior parte do dia a dia de distribuidoras e empresas de serviços — transformar um PDF de fornecedor ou uma planilha de cliente em uma cotação limpa e com preço — e torna isso uma operação de um clique. E segue em frente: reserva, notas de entrega, devoluções, recebimentos de estoque e um assistente embutido que responde perguntas sobre os seus próprios dados. A DHD Tech desenhou, construiu e colocou o Quotery no ar. Este post é o tour de engenharia por trás do lançamento.
A promessa pública na home é "Cote mais rápido. Entregue com certeza." Por trás dessa frase existem três números que o time de produto defende em público: 70% de tempo economizado na entrada de cotação, 99,2% de acurácia no match de itens e três idiomas suportados (en-US, pt-BR, es-US). O texto abaixo é como esses números são conquistados — e por que a arquitetura ainda vai parecer sã daqui a dois anos.
Apresentando o Quotery
O Quotery é voltado para distribuidores, empreiteiras e oficinas de serviço que trabalham em ritmo alto. A dor diária é conhecida de qualquer um que já trabalhou dentro de uma dessas operações. Um cliente manda uma lista de compras como um PDF bagunçado, ou um fornecedor manda um XLSX que não se parece com o anterior. Um comercial redigita linha a linha no ERP que a empresa adotou anos atrás, casando cada linha com um SKU interno na base do olho e torcendo para o preço estar atualizado. Aí a entrega acontece em outro lugar — no papel, em outra ferramenta, ou na cabeça de alguém — e quando o estoque finalmente se mexe, três sistemas diferentes discordam sobre o que foi vendido.
O Quotery comprime tudo isso em uma única plataforma com isolamento por tenant. Um documento entra pelo endpoint de import com IA, o sistema resolve deterministicamente tudo que consegue contra o seu catálogo, pede para um LLM decidir o resto e deixa você numa cotação em rascunho com cada linha classificada. Quando o comercial aprova, fechar a cotação reserva estoque. Registrar uma nota de entrega consome. Uma nota de devolução devolve. Um recebimento de estoque incrementa. Cada movimento escreve uma linha de razão append-only e um evento de auditoria imutável. Nada nesse ciclo precisa de um segundo sistema.
Os números no site refletem a forma real de uso: a entrada é a etapa cara, então os 70% de tempo economizado vêm principalmente do import com IA fazendo a redigitação por você. Os 99,2% de acurácia são a soma do match determinístico de código com o pick em lote do LLM — detalhes abaixo. E a UI trilíngue existe porque os primeiros clientes do Quotery operam entre Estados Unidos, Brasil e Caribe dentro do mesmo dia útil.
O Processo Racional Por Trás
O backend do Quotery impõe uma arquitetura em camadas em que cada app Django é dividido em quatro pacotes: models, utils, services, views. Isso não é preferência; é imposto por convenção e pelo formato da suíte de testes. Cada camada tem exatamente uma responsabilidade, e o fluxo de dados é de mão única na entrada e de mão única na saída.
Models
Só schema. Definição de campos, Meta, __str__. Todo modelo de negócio herda de core.models.BaseModel, que já fornece chave primária UUIDField, created_at, updated_at, um flag is_deleted de soft-delete e um método soft_delete(). O User é a única exceção documentada. Nenhuma lógica de negócio mora nessa camada, e não existem save() customizados escondendo efeitos colaterais do resto do código.
Utils
A única camada autorizada a tocar no ORM. Se você vê .objects, .filter(), .create(), .save() ou .delete(), você está em um módulo utils/. O naming é formulaico — build_<entidade>_queryset, apply_<campo>_filter, create_<entidade>, update_<entidade>, delete_<entidade> — para que um revisor adivinhe o conteúdo do arquivo antes de abri-lo.
Services
Regras de negócio e validação. Services compõem utils; nunca falam com o ORM diretamente. Payloads passam por um whitelist ALLOWED_FIELDS para que um cliente jamais injete tenant, is_deleted ou similar por uma chave JSON perdida. ValueError sinaliza falha de validação; registros ausentes viram Model.DoesNotExist. Todo service que mexe em estoque roda dentro de transaction.atomic() e trava as linhas de StockItem com select_for_update() em ordem determinística por product-id e location-id, para ficar livre de deadlock sob fechamentos concorrentes.
Views
DRF GenericViewSets que fazem três coisas: autenticam, deserializam, despacham para um service. Erros viram códigos HTTP linha a linha — ValueError vira 400, DoesNotExist vira 404, PermissionDenied vira 403. A paginação é a PageNumberPagination padrão do DRF; nada é artesanal.
Os testes espelham as camadas um para um: test_<entidade>_models.py, test_<entidade>_utils.py, test_<entidade>_services.py, test_<entidade>_views.py. Uma falha já te diz em qual camada o bug mora. Lint é ruff no container api; o pre-commit do host tem black, isort, flake8 e bandit; o Swagger via drf-spectacular só é montado em local e dev, então produção nunca expõe um endpoint de introspecção.
A Stack
As escolhas são deliberadamente sem graça. Sem graça se acumula com o tempo.
- Backend: Python 3.12, Django 5, DRF 3.15, PostgreSQL 17, Redis 7, Gunicorn.
- Frontend: React 19, Vite 7, TypeScript, Tailwind CSS, Framer Motion, react-i18next.
- Auth: django-allauth (Google OAuth) para a troca via SPA, djangorestframework-simplejwt quando um fluxo JWT clássico é necessário, cookies de sessão marcados HttpOnly para que o SPA não consiga lê-los via JavaScript.
- Admin: django-unfold com a marca do Quotery; Swagger via drf-spectacular montado apenas em local e dev.
- IA: o SDK da OpenAI apontando para
gpt-4.1-minipara orquestração de import egpt-image-1para imagens de apoio. - Documentos: WeasyPrint para gerar PDF, python-magic para detecção de MIME, pypdf e openpyxl para parsear PDF / XLSX / XLS / CSV de entrada.
- Infraestrutura: Docker Compose para rodar a stack completa localmente, Render.com para o deploy em nuvem. O frontend e a landing são sites estáticos independentes no Render; a API é um web service no Render.
A regra para qualquer nova dependência é um ADR de um parágrafo em docs/decisions/. Se não vale um parágrafo, não vale o install.
Cache Que Não Mente
O Redis fica na frente de toda leitura cara e é a parte do sistema que mais silenciosamente erra em software multi-tenant. A regra do Quotery é que toda chave de cache passa por um único helper — core.cache.make_key — que produz chaves no formato qf:v1:<grupo>:<escopo>:<hash-dos-params>. <escopo> é um UUID de tenant, um segmento user:<user_id> para caches por usuário como o payload do usuário atual, ou a palavra literal global para leituras de plataforma. Misturar escopos em uma única chamada levanta ValueError. Um cache.set("dashboard", ...) solto é inalcançável.
A segunda regra é invalidação em vez de TTL. Todo service que muda dado em cache termina seu bloco atômico com uma chamada explícita invalidate_<grupo>(tenant_id=...). TTLs são rede de segurança, não o critério de correção. Um guarda estático varre cada função de service com @transaction.atomic e falha o build se o corpo não invalida nem se declara cache-safe. Esse guarda também cobre management commands e ModelAdmin, já que os dois ignoram a camada de service. Com essa rede no lugar, os TTLs server-side foram subidos para 24 horas em todos os grupos, exceto o cache me sensível a autorização, que fica em uma hora. Se uma mudança de papel falhar em invalidar o cache, o pior caso é um usuário ver permissões antigas por no máximo uma hora — não um dia.
A terceira regra é soft-fail. cache_get_or_set envolve cache.get e cache.set em try-except e cai no producer em qualquer exceção de backend, logando um WARNING no logger core.cache. Um incidente no Redis vira regressão de latência, não indisponibilidade. TTLs por grupo e o fan-out de invalidação vivem em uma matriz ao lado do módulo de cache, então toda nova mutação que esquece de atualizar a matriz é pega em revisão.
Import com IA: Três Chamadas em Uma Transação Atômica
Essa é a feature que conquista os 70% de tempo economizado. O usuário sobe um documento — PDF, XLSX, XLS, CSV ou texto colado — e o sistema devolve uma cotação em rascunho com cada linha classificada e um resumo legível. Todo o fluxo roda síncrono dentro de um único bloco @transaction.atomic, então se qualquer coisa falha, nada persiste.
Call A: extrai a estrutura
Texto normalizado entra, um payload tipado {groups, ungrouped_items} sai. A chamada usa o SDK da OpenAI com response format em JSON-schema estrito, então o modelo não pode devolver dado com forma diferente. Quando Call A retorna vazio, o orquestrador levanta ValueError("no_items_detected"), e a transação atômica em volta converte isso em um rollback limpo da linha vazia de Quote que senão vazaria para o banco.
Match determinístico
Antes do modelo ver qualquer produto, o Quotery roda igualdade exata e case-sensitive contra quatro colunas de código em Product: sku, import_code, internal_code e export_code. Cada coluna tem um índice GIN com trigramas. Todo hit vira uma linha EXACT_MATCH e pula o LLM por completo. É daqui que a maior parte dos números de custo e acurácia vem — catálogos com códigos bem mantidos resolvem a maioria das linhas de graça.
Shortlist e Call B: pick-or-reject em lote
Para toda linha que o passo determinístico perdeu, o Quotery monta uma shortlist de até 15 produtos candidatos usando os primeiros oito tokens da descrição contra o nome e as quatro colunas de código. Depois disso, todas as linhas não resolvidas são mandadas ao LLM em uma única chamada em lote. O modelo escolhe um id de candidato por linha ou devolve uma rejeição. Empacotar as decisões troca várias idas e voltas por uma — ganho de custo e latência que cresce com o tamanho do documento.
Todo id retornado é validado contra a shortlist antes de encostar no banco. Se o modelo alucina um id que nunca esteve no conjunto de candidatos, ou um id de outro tenant que vazou de alguma forma, essa linha cai para NOT_FOUND em vez de casar com o produto errado. Essa proteção importa: uma cotação errada em escala é mais cara do que uma cotação com uma linha manual deixada para o comercial.
Call C: o banner de resumo
Depois que as linhas persistem, o Quotery pede um resumo de uma a três frases no idioma do usuário — inglês, português ou espanhol. O resumo é efêmero. Ele volta dentro do envelope da resposta HTTP, é renderizado uma única vez em um banner na página de detalhe da cotação, e é limpo ao dar reload. Nunca é escrito no banco, então a feature não tem migration e não tem história de limpeza.
Chips de match-kind e a garantia de atomicidade
Cada linha persistida carrega um import_match_kind: EXACT_MATCH, AI_DECISION, NOT_FOUND ou MANUAL para linhas digitadas à mão. O SPA renderiza um chip ao lado do rótulo do produto para o comercial ver na hora como cada linha foi classificada. Nada no import com IA reserva estoque — cotações importadas caem em rascunho, o comercial revisa, e o fluxo de fechamento padrão cuida da reserva com as garantias de concorrência descritas abaixo.
Ciclo de Vida da Cotação + Razão de Estoque
Uma Quote vive em uma máquina de estados: draft → sent → closed → partially_delivered → delivered, com ramos de cancelled saindo de draft, sent e closed. A numeração por tenant e por ano usa um alocador sem gaps — Q-YYYY-NNNN — suportado por um SELECT ... FOR UPDATE em uma linha de QuoteNumberSequence, para que dois fechamentos concorrentes no mesmo tenant-ano não colidam. Cancelar uma cotação fechada escreve linhas RELEASE de razão desfazendo a reserva; cancelar uma parcialmente ou totalmente entregue devolve 409 e te manda fazer uma nota de devolução.
O estoque é rastreado em um razão append-only. Toda mudança em um StockItem escreve uma linha StockMovement com kind tipado — RECEIPT, DELIVERY, ADJUSTMENT, RETURN — um delta com sinal e FKs opcionais para o documento origem. O razão é append-only até para staff: StockMovement._meta.default_permissions está vazio, e o admin sobrescreve has_add_permission, has_change_permission e has_delete_permission para False. Ajustes manuais passam por um endpoint dedicado POST /api/stock/{id}/adjust/, admin-only, que exige notas não vazias.
Dois invariantes merecem destaque. Primeiro, on_hand pode ficar negativo — backorders são coisa real nos negócios que o Quotery atende, e esconder isso atrás de um 409 é pior do que mostrar. Segundo, reserved pode passar de on_hand: fechar uma cotação quando o armazém está curto não devolve 409; devolve uma lista shortages[] na resposta para o comercial agir. A checagem de disponibilidade acontece na entrega, onde deveria — no momento em que o estoque de fato se move. Multi-localização é nativo: cada StockItem é chaveado por Location, locations são por tenant, e a localização default é auto-criada quando um Tenant nasce.
A entrega roda em três documentos com uma máquina de estados compartilhada draft-para-posted e numeração por tenant e por ano em uma única tabela DocumentNumberSequence chaveada por (tenant, prefixo, ano). Notas de entrega usam DN-YYYY-NNNN; postar uma consome on_hand e reserved e move a cotação pai para partially_delivered ou delivered. Notas de devolução usam RN-YYYY-NNNN; postar uma adiciona em on_hand apenas e deixa a cotação pai em paz. Recebimentos de estoque usam SR-YYYY-NNNN; são entradas de fornecedor, não têm link para cotação e adicionam em on_hand.
RBAC, Auditoria, Links de Compartilhamento e Respostas
A autorização roda em um catálogo customizado de permissões mais um mapeamento grupo-permissão. Cotações têm visibilidade gated por auth.Group: usuários comerciais veem as próprias cotações mais as que o grupo compartilha; admin e manager veem tudo do tenant. Usuários de armazém veem estoque e documentos de entrega, mas não vêem a superfície de vendas. Toda busca cross-tenant devolve 404 em vez de 403, para a API nunca confirmar a existência de registros em outro tenant.
Toda mudança de negócio escreve um AuditEvent imutável na mesma transação da escrita. StockMovement é a trilha de auditoria de estoque; AuditEvent é a trilha de auditoria de todo o resto. Juntos, dão a um operador a reconstrução completa de quem fez o quê, quando e em qual registro.
A exportação de PDF passa por WeasyPrint — a página de detalhe da cotação tem um botão de "baixar PDF" que renderiza o mesmo layout que o cliente vê em um link público. QuoteShare emite uma URL pública read-only com token rotacionado, então o comercial pode mandar um link que sobrevive ao fechar da aba sem abrir o resto do tenant para tráfego anônimo. O formulário de contato em /contact grava envios anônimos em ContactMessage para o time da plataforma triar.
A UI é totalmente trilíngue — en-US, pt-BR e es-US — com rotas prefixadas por idioma no SPA. Um cookie compartilhado qf-language no domínio pai .quotery.io leva a escolha do usuário entre a landing, o app e qualquer subdomínio futuro. E o produto já nasce com "Respostas, não dashboards": um assistente embutido que deixa o usuário perguntar em linguagem natural sobre as próprias cotações, clientes e estoque. Quando um comercial quer saber quais três clientes compraram mais de um SKU neste trimestre, ele pergunta; o assistente monta a consulta, as regras de tenant mantêm a resposta dentro do tenant, e a resposta volta como uma frase e uma tabela pequena. Sem BI. Sem exportação.
O Que Vem Depois
- Mover o import síncrono com IA para trás de um worker em background para documentos com mais de algumas centenas de linhas, transformando a pilha de timeout de 180 segundos em não-problema.
- Mapear erros do SDK da OpenAI em toasts traduzidos no SPA em vez de vazá-los como 500 genéricos.
- Publicar uma superfície REST pública e estável sob um prefixo versionado para tenants que querem integrar o Quotery aos próprios ERPs.
- Adicionar streaming dos estágios intermediários do import para a página de processamento refletir progresso real em vez de tempo estimado.
- Expandir o grounding do assistente embutido para cobrir documentos de entrega, não só cotações e estoque.
Experimente
O Quotery está em produção agora em https://www.quotery.io. O tour do produto, os preços e o cadastro vivem na landing. Se o seu time está perdendo horas na entrada de cotação e quer uma plataforma com isolamento por tenant rodando o resto do ciclo, fale com a gente em https://www.quotery.io/contact. A DHD Tech construiu; também toca o roadmap; e estamos aceitando um número pequeno de parceiros iniciais no nível de integrações.