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>
349 lines
12 KiB
Markdown
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.*
|