Formas de pagamento: - Endpoint GET /catalog/payment-methods lendo vw_formas_pagamento filtrado por ativa=1 e integrar_sar=1 - FormaPagamento schema/type no shared api-interface - Hook useFormasPagamento (staleTime 1h) substituindo lista hardcoded Offline (FR-4.2 / NFR-2.1–2.4): - IndexedDB queue: lib/offline/idb.ts + order-queue.ts sem deps externos - NewOrderPage detecta !navigator.onLine → enqueueOrder() → toast + reset - useOfflineSync: auto-sync ao reconectar (POST orders + PATCH transmit) - usePendingOrders: fila reativa via CustomEvents - AppShell: banner offline + useOfflineSync() global - OrdersPage: seção de pedidos pendentes com retry/descartar - sw.js: network-first para API GETs cacheáveis + stale-while-revalidate para assets + app shell navigate fallback Docs: - architecture.md: documento de decisões de arquitetura do SAR MVP Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
12 KiB
stepsCompleted, inputDocuments, workflowType, project_name, user_name, date
| stepsCompleted | inputDocuments | workflowType | project_name | user_name | date | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
architecture | SAR — Força de Vendas | Julian | 2026-05-30 |
Documento de Decisões de Arquitetura
SAR — Força de Vendas
Referência canônica para agentes de IA e devs implementando histórias. Cada decisão aqui é lei — não reabrir sem RFC.
1. Visão geral do sistema
SAR é um SaaS B2B de força de vendas multi-tenant. Arquitetura web-first com PWA para o cockpit Rep (mobile), desktop para Supervisor/Dono. MVP entrega cockpits Rafael (Rep) e Sandra (Supervisor).
┌─────────────────────────────────────────────────────────┐
│ Browser (PWA/Desktop) │
│ React 19.2 + TanStack Router/Query + Zustand + AntD │
└──────────────────────┬──────────────────────────────────┘
│ REST /api/v1 (+WebSocket/SSE futuro)
┌──────────────────────▼──────────────────────────────────┐
│ NestJS 11 API (apps/api) │
│ JwtAuthGuard → WorkspaceCLS → Module handlers │
└──────────────────────┬──────────────────────────────────┘
│ Prisma 7 (pool por empresa)
┌──────────────────────▼──────────────────────────────────┐
│ PostgreSQL 18 (schema "sar" + views ERP) │
│ Um schema por empresa (idEmpresa) │
└─────────────────────────────────────────────────────────┘
2. Estrutura do monorepo
sar/
├── apps/
│ ├── api/ ← NestJS (backend)
│ └── web/ ← React Vite (frontend)
├── libs/
│ └── shared/
│ └── api-interface/ ← contratos Zod compartilhados
├── STACK.md ← fonte da verdade técnica
└── CODING-RULES.md ← invariantes e pegadinhas
Regra: Qualquer tipo que cruza a fronteira API↔Web mora em libs/shared/api-interface. Nunca duplicar tipos.
3. Multi-tenancy
Modelo: idEmpresa: number (inteiro do ERP) identifica o tenant. Não existe workspaceId: string — o ADR 0006 original foi revogado.
Mecanismo no backend:
WorkspaceModuleregistra CLS global (nestjs-cls)- CLS middleware injeta
requestIdeidEmpresa = 0(fallback para rotas públicas) JwtAuthGuardapós validar o JWT sobrescreveidEmpresa,userId,rolee injeta oPrismaClientcerto no CLS- Todo handler acessa
cls.get('prisma')— nunca um Prisma singleton (CODING-RULES PGD-DB-009)
No banco:
- Schema
sarcontém todas as tabelas SAR - Views
vw_*expõem dados do ERP legado (leitura apenas) - Isolation por
id_empresanas queries — nenhuma query atravessa empresas
4. Módulos da API (NestJS)
| Módulo | Responsabilidade |
|---|---|
auth |
/auth/me — perfil do usuário autenticado; /dev-auth — login dev (sem master-login) |
workspace |
CLS global, pool de Prisma por empresa |
catalog |
Catálogo de produtos, empresa (pauta/preço) |
clients |
Lista e ficha de clientes |
orders |
CRUD pedidos, fluxo aprovação, histórico |
dashboard |
KPIs do Rep e Supervisor |
notifications |
Web Push (futuro SSE/Socket.IO) |
health |
/health — liveness/readiness |
ping |
/ping — sanity check |
logger |
Pino configurado com redact de PII |
Padrão de módulo:
módulo/
├── *.module.ts
├── *.controller.ts ← valida entrada com contrato Zod
├── *.service.ts ← lógica de domínio
└── *.types.ts ← tipos internos (não exportados para o shared)
5. Contratos Zod (API Interface)
Localização: libs/shared/api-interface/src/lib/*.contract.ts
Contratos existentes: auth, client, company, dashboard, notifications, order, ping, product
Padrão de contrato:
// Schema de entrada (mutation)
export const CreateXxxSchema = z.object({ ... });
export type CreateXxx = z.infer<typeof CreateXxxSchema>;
// Schema de saída (query)
export const XxxResponseSchema = z.object({ ... });
export type XxxResponse = z.infer<typeof XxxResponseSchema>;
// Schema de lista (com paginação)
export const XxxListQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(200).default(50),
});
export const XxxListResponseSchema = z.object({
data: z.array(XxxResponseSchema),
total: z.number().int().nonneg(),
page: z.number().int().positive(),
limit: z.number().int().positive(),
});
Regra: Backend valida entrada com o Schema (422 se inválido — RFC 9457). Frontend parseia resposta com o Schema (Schema.parse(data)).
6. Autenticação e autorização
Fluxo MVP (dev):
- Token JWT em
localStorageviaauthStore(transitório — dev only) POST /api/v1/auth/dev-loginretorna JWT assinado- Bearer token no header de toda requisição protegida
Fluxo produção (planejado):
- master-login IdP OAuth2/OIDC (JCS próprio)
- Access token em memória; refresh token em cookie
httpOnly; Secure; SameSite=Lax JwtAuthGuardvalida e injeta contexto no CLS
Papéis (roles): rep · supervisor · dono · admin
Frontend — role routing:
- Raiz
/→ lê role do JWT payload, redireciona para<RepPainel>ou<SupervisorPainel> - Cada cockpit renderiza apenas suas rotas; sem RBAC granular no MVP
7. Frontend — estrutura
apps/web/src/
├── cockpits/
│ ├── rep/ ← páginas do cockpit Rafael
│ │ ├── RepPainel.tsx
│ │ ├── ClientsPage.tsx
│ │ ├── ClientDetailPage.tsx
│ │ ├── OrdersPage.tsx
│ │ ├── OrderDetailPage.tsx
│ │ ├── OrderPrintPage.tsx
│ │ ├── NewOrderPage.tsx
│ │ └── CatalogPage.tsx
│ └── supervisor/ ← páginas do cockpit Sandra
│ ├── SupervisorPainel.tsx
│ └── ApprovalQueuePage.tsx
├── components/
│ └── layout/ ← AppShell, Sidebar, Topbar
├── lib/
│ ├── router.tsx ← TanStack Router (flat routes)
│ ├── auth-store.ts ← Zustand-lite para token
│ ├── api-client.ts ← apiFetch + ApiError (RFC 9457)
│ ├── query-client.ts← TanStack Query config
│ ├── queries/ ← hooks de query por domínio
│ ├── hooks/ ← hooks utilitários
│ └── theme.ts ← AntD theme config
Rotas atuais:
| Path | Componente | Cockpit |
|---|---|---|
/ |
HomeRoute (role redirect) | — |
/rep |
RepPainel | Rep |
/clientes |
ClientsPage | Rep |
/clientes/$id |
ClientDetailPage | Rep |
/pedidos |
OrdersPage | Rep |
/pedidos/novo |
NewOrderPage | Rep |
/pedidos/$id |
OrderDetailPage | Rep |
/pedidos/$id/imprimir |
OrderPrintPage | Rep |
/catalogo |
CatalogPage | Rep |
/aprovacoes |
ApprovalQueuePage | Supervisor |
8. Gerenciamento de estado
| Tipo de estado | Solução |
|---|---|
| Server state (queries, mutations) | TanStack Query v5 |
| Auth (token, user profile) | Zustand (authStore) |
| UI state local (modais, forms) | useState / useReducer em componente |
| Offline queue (futuro) | IndexedDB + Service Worker |
Regra: Não duplicar server state em Zustand. TanStack Query é a fonte da verdade para dados do servidor.
9. API Client (frontend)
lib/api-client.ts — apiFetch(path, options):
- Base URL:
/api/v1(proxy Vite em dev →:3000; Nginx em prod → mesmo origin) - Injeta
Authorization: Bearer <token>automaticamente - Parseia
application/problem+json→ApiErrorcomstatus+problem - Não faz parse Zod — caller é responsável
Tratamento de erro:
422= validação Zod (erros detalhados emproblem.errors)4xxoutros = erro de domínio5xx= retry automático pelo QueryClient (máx 2x)
10. Erros e RFC 9457
Todo erro da API retorna application/problem+json:
{
"type": "https://sar.jcsinformatica.com.br/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "Campo obrigatório ausente",
"requestId": "uuid",
"errors": [{ "path": "idCliente", "message": "Required", "code": "invalid_type" }]
}
Regra (CODING-RULES): Nunca retornar 403 para recurso inexistente — usar 404 para não vazar existência.
11. Ciclo de vida do Pedido
Estados controlados pelo SAR:
0 (Orçamento) ──→ 1 (Ag. Aprovação) ──→ 2 (Transmitido)
│
└──→ 0 (recusado → volta Orçamento)
Estados espelhados do ERP (pós-integração): 3=Cancelado, 4=Faturado
Alçada: se descontoPerc > alçadaRep → situa vai para 1. Supervisor aprova/recusa/ajusta.
Idempotency-Key: gerado no frontend antes do envio; garante que retentativas de sync não duplicam pedidos.
12. Impressão de pedido
Rota /pedidos/$id/imprimir → OrderPrintPage.tsx — renderiza HTML otimizado para window.print() (browser → PDF). Sem geração server-side de PDF no MVP.
13. Offline (Rafael) — planejado
Mecanismo: IndexedDB queue + Service Worker
- Catálogo e clientes cacheados com TTL 4h
- Pedidos criados offline enfileirados com
Idempotency-Keylocal - Sync automático ao retorno de sinal
- Dados financeiros sensíveis (limite de crédito numérico, inadimplência) só online
Status atual: não implementado. Será uma epic dedicada.
14. Real-time (Sandra) — planejado
Mecanismo: Socket.IO 4 / SSE via notifications module
- Novos pedidos aparecem em < 3s no painel Sandra
- Push notifications Web Push (PWA) para aprovações
Status atual: notifications.module.ts existe, endpoints WIP.
15. Banco de dados
- PostgreSQL 18, schema
sar - Prisma 7 como ORM/query builder
- Pool de clientes Prisma gerenciado pelo
WorkspacePrismaPool(CLS) - Views
vw_clientes,vw_representantes,vw_pedidos_erpexpõem dados legados - Isolation por
id_empresaem todas as queries
Regra: Raw SQL via prisma.$queryRaw apenas quando Prisma ORM não atende (ex: JOINs com views legadas).
16. Infraestrutura
| Componente | Solução |
|---|---|
| Hosting | Proxmox on-prem Brasil |
| Orquestração | Docker Compose |
| Deploy | Ansible |
| CDN / proxy | Cloudflare + Nginx |
| Object storage | MinIO |
| Secrets | Vault |
| Cache / filas | Valkey (Redis-compatible) + BullMQ 5.77 |
| Observabilidade | OpenTelemetry + Pino |
| Resend |
17. Invariantes de implementação (CODING-RULES)
PrismaClientsempre viacls.get('prisma')— nunca singleton injetado diretamenteidEmpresareal vem do JWT (guard) — nunca de.envou parâmetro de rota- PII (CPF/CNPJ, telefone, e-mail) redactada em todos os logs via Pino
redact - Tokens JWT: acesso em memória, refresh em cookie
httpOnly; Secure; SameSite=Lax - Rate limit em auth: 5 tentativas/min/IP
- Nomes visíveis em toda UI — nunca exibir só código (cliente, rep, produto)
- Empresa matriz
9001normalizada →1em consultas de catálogo
18. Decisões em aberto (não resolver sem RFC)
| # | Questão |
|---|---|
| OQ-1 | Formato e frequência de importação do catálogo/clientes do ERP legado |
| OQ-2 | Alçada de desconto: fixa por rep ou por linha de produto? |
| OQ-3 | Fórmula de cálculo de comissão FLEX documentada? |
| OQ-5 | Múltiplos supervisores: distribuição da fila de Aprovações |
| OQ-6 | TTL de 4h para catálogo offline é aceitável para o 1º cliente? |
Gerado em 2026-05-30. Baseado no código existente + PRD 2026-05-27 + Product Brief 2026-05-26.