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>
This commit is contained in:
348
_bmad-output/planning-artifacts/architecture.md
Normal file
348
_bmad-output/planning-artifacts/architecture.md
Normal file
@@ -0,0 +1,348 @@
|
||||
---
|
||||
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.*
|
||||
Reference in New Issue
Block a user