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