Files
sar/_bmad-output/planning-artifacts/architecture.md
julian a3c68f9f05 feat(mvp-rep): formas de pagamento do ERP + suporte offline completo
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>
2026-05-30 21:30:23 +00:00

349 lines
12 KiB
Markdown

---
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<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 `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 `<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``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.*