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

12 KiB

stepsCompleted, inputDocuments, workflowType, project_name, user_name, date
stepsCompleted inputDocuments workflowType project_name user_name date
1
2
3
4
5
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
architecture SAR — Força de Vendas Julian 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:

// 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.tsapiFetch(path, options):

  • Base URL: /api/v1 (proxy Vite em dev → :3000; Nginx em prod → mesmo origin)
  • Injeta Authorization: Bearer <token> automaticamente
  • Parseia application/problem+jsonApiError 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:

{
  "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/imprimirOrderPrintPage.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.