--- stepsCompleted: [1, 2, 3, 4, 5] inputDocuments: - design-artifacts/A-Product-Brief/01-product-brief.md - _bmad-output/planning-artifacts/prds/prd-sar-2026-05-27/prd.md - design-artifacts/C-UX-Scenarios/00-ux-scenarios.md - design-artifacts/_progress/wds-project-outline.yaml workflowType: 'architecture' project_name: 'SAR — Força de Vendas' user_name: 'Julian' date: '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:** - `WorkspaceModule` registra CLS global (nestjs-cls) - CLS middleware injeta `requestId` e `idEmpresa = 0` (fallback para rotas públicas) - `JwtAuthGuard` após validar o JWT sobrescreve `idEmpresa`, `userId`, `role` e injeta o `PrismaClient` certo no CLS - Todo handler acessa `cls.get('prisma')` — nunca um Prisma singleton (CODING-RULES PGD-DB-009) **No banco:** - Schema `sar` contém todas as tabelas SAR - Views `vw_*` expõem dados do ERP legado (leitura apenas) - Isolation por `id_empresa` nas 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:** ```typescript // Schema de entrada (mutation) export const CreateXxxSchema = z.object({ ... }); export type CreateXxx = z.infer; // Schema de saída (query) export const XxxResponseSchema = z.object({ ... }); export type XxxResponse = z.infer; // 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 `localStorage` via `authStore` (transitório — dev only) - `POST /api/v1/auth/dev-login` retorna 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` - `JwtAuthGuard` valida e injeta contexto no CLS **Papéis (roles):** `rep` · `supervisor` · `dono` · `admin` **Frontend — role routing:** - Raiz `/` → lê role do JWT payload, redireciona para `` ou `` - 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 ` automaticamente - Parseia `application/problem+json` → `ApiError` com `status` + `problem` - Não faz parse Zod — caller é responsável **Tratamento de erro:** - `422` = validação Zod (erros detalhados em `problem.errors`) - `4xx` outros = erro de domínio - `5xx` = retry automático pelo QueryClient (máx 2x) --- ## 10. Erros e RFC 9457 Todo erro da API retorna `application/problem+json`: ```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-Key` local - 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_erp` expõem dados legados - Isolation por `id_empresa` em 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 | | E-mail | Resend | --- ## 17. Invariantes de implementação (CODING-RULES) - `PrismaClient` sempre via `cls.get('prisma')` — nunca singleton injetado diretamente - `idEmpresa` real vem do JWT (guard) — nunca de `.env` ou 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 `9001` normalizada → `1` em 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.*