refactor(erp): integração direta com banco ERP — schema sar
Revoga ADR 0006 (BD-por-workspace separado). O SAR agora conecta ao banco PostgreSQL do ERP (módulo SIG) e usa o schema `sar` para tudo. PRISMA - Remove: Client, Product, Order, OrderItem, OrderStatusHistory, RepTarget, RepDiscountLimit, PushSubscription (modelos isolados) - Adiciona: Pedido, PedidoItem, HistoricoPedido, AlcadaDesconto, MetaRepresentante, PushSubscription (mapeados para sar.*) - IDs: id_cliente/cod_vendedor/id_empresa são INTEGER (ERP) - situa: Int (1=Pendente 2=Aprovado 3=Cancelado 4=Faturado) - JWT: workspace_id:string → id_empresa:number - URL: inclui ?schema=sar para Prisma rotear ao schema ERP SERVICES - ClientsService: $queryRawUnsafe contra sar.vw_clientes + sar.pedidos - CatalogService: $queryRawUnsafe contra sar.vw_produtos + sar.vw_estoque - OrdersService: Prisma models Pedido/PedidoItem/HistoricoPedido/AlcadaDesconto - DashboardService: MetaRepresentante + queries raw para inativos - NotificationsService: PushSubscription com codVendedor + idEmpresa CONTRATOS (api-interface) - client.contract: campos ERP (idCliente, nome, cgcpf, cod_vendedor…) - order.contract: PedidoSummary/PedidoDetail/CreatePedido + SITUA_LABEL - product.contract: ProdutoSummary/ProdutoDetail (vw_produtos) - auth.contract: workspaceId:string → idEmpresa:number WEB - Todos os cockpits e queries atualizados para os novos tipos Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
// SAR — Workspace Database Schema
|
||||
// Stack canon: Prisma 7 · PostgreSQL 18 · BD-por-workspace (ADR 0006)
|
||||
//
|
||||
// Este schema roda em CADA workspace DB (sar_workspace_<id>).
|
||||
// NÃO há workspaceId/tenantId em nenhum modelo — o isolamento é físico.
|
||||
// O banco master (sar_master) é gerenciado pelo master-login (IdP JCS), não por este schema.
|
||||
// SAR — Schema no banco ERP da JCS (schema `sar` dentro do PostgreSQL do SIG/gestao)
|
||||
// ADR 0006 revogado: banco separado por workspace → schema `sar` no ERP JCS.
|
||||
// O isolamento multi-tenant é por `id_empresa` em todas as tabelas.
|
||||
//
|
||||
// CODING-RULES PGD-DB-004: moduleFormat = "cjs" (NestJS é CJS)
|
||||
// CODING-RULES PGD-DB-001: MIGRATION_DATABASE_URL aponta direto ao PG (sem PgBouncer)
|
||||
// A URL de runtime deve incluir ?schema=sar (injetado pelo JwtAuthGuard via WorkspacePrismaPool)
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -16,232 +14,156 @@ generator client {
|
||||
|
||||
// Prisma 7: url removida do schema — conexão em prisma.config.ts (migrate)
|
||||
// e no WorkspacePrismaPool via PrismaPg adapter (runtime).
|
||||
// A URL de runtime inclui ?schema=sar para rotear ao schema correto.
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ─── Enums ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Situação financeira resumida do cliente — cacheável offline (FR-2.4, FR-2.5).
|
||||
enum FinancialStatus {
|
||||
regular
|
||||
attention
|
||||
blocked
|
||||
}
|
||||
|
||||
// Status do pedido (FR-3.2). Transições: budget → pending_approval → approved → invoiced.
|
||||
// Qualquer status pode ir para cancelled.
|
||||
enum OrderStatus {
|
||||
budget // orçamento
|
||||
pending_approval // aprovação pendente
|
||||
approved // aprovado
|
||||
invoiced // faturado
|
||||
cancelled // cancelado
|
||||
}
|
||||
|
||||
// ─── Client (C2) ─────────────────────────────────────────────────────────────
|
||||
// ─── Pedido (C3) ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Cadastro sincronizado do ERP legado (FR-2.6). Rep não cria/edita no MVP.
|
||||
// creditLimit: gerenciado no SAR (OQ-4 resolvido 2026-05-27).
|
||||
// lastOrderAt/lastOrderValue/openOrdersCount: desnormalizados de Orders.
|
||||
// Pedido emitido pelo Rep. Situa: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado.
|
||||
// idEmpresa: tenant (empresa no ERP). codVendedor: gestao.vendedor.codigo.
|
||||
// idCliente: sig.corrent.id_corrent. numPedSar: sequencial SAR (SAR-NNNNN).
|
||||
|
||||
model Client {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String
|
||||
tradeName String?
|
||||
taxId String @unique
|
||||
email String?
|
||||
phone String?
|
||||
address Json?
|
||||
model Pedido {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
idEmpresa Int @map("id_empresa")
|
||||
numPedSar String @unique @map("num_ped_sar")
|
||||
idCliente Int @map("id_cliente")
|
||||
codVendedor Int @map("cod_vendedor")
|
||||
situa Int @default(1)
|
||||
dtPedido DateTime @default(now()) @db.Date @map("dt_pedido")
|
||||
idPauta Int? @map("id_pauta")
|
||||
codFormapag Int? @map("cod_formapag")
|
||||
totalProdutos Decimal @default(0) @db.Decimal(15, 2) @map("total_produtos")
|
||||
totalIpi Decimal @default(0) @db.Decimal(15, 2) @map("total_ipi")
|
||||
totalIcmsst Decimal @default(0) @db.Decimal(15, 2) @map("total_icmsst")
|
||||
total Decimal @default(0) @db.Decimal(15, 2)
|
||||
descontoPerc Decimal @default(0) @db.Decimal(5, 2) @map("desconto_perc")
|
||||
descontoValor Decimal @default(0) @db.Decimal(15, 2) @map("desconto_valor")
|
||||
acrescimo Decimal @default(0) @db.Decimal(15, 2)
|
||||
comissao Decimal @default(0) @db.Decimal(15, 2)
|
||||
pedFlex Decimal @default(0) @db.Decimal(15, 2) @map("ped_flex")
|
||||
obs String?
|
||||
aprovadoPor Int? @map("aprovado_por")
|
||||
aprovadoEm DateTime? @map("aprovado_em")
|
||||
motivoRecusa String? @map("motivo_recusa")
|
||||
idempotencyKey String? @unique @map("idempotency_key")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
financialStatus FinancialStatus @default(regular)
|
||||
creditLimit Decimal? @db.Decimal(15, 2)
|
||||
itens PedidoItem[]
|
||||
historico HistoricoPedido[]
|
||||
|
||||
repId String
|
||||
lastOrderAt DateTime?
|
||||
lastOrderValue Decimal? @db.Decimal(15, 2)
|
||||
openOrdersCount Int @default(0)
|
||||
|
||||
erpCode String?
|
||||
syncedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
orders Order[]
|
||||
|
||||
@@index([repId])
|
||||
@@index([taxId])
|
||||
@@index([name])
|
||||
@@index([deletedAt])
|
||||
@@index([idEmpresa])
|
||||
@@index([codVendedor])
|
||||
@@index([idCliente])
|
||||
@@index([situa])
|
||||
@@index([dtPedido])
|
||||
@@map("pedidos")
|
||||
}
|
||||
|
||||
// ─── Order (C3) ──────────────────────────────────────────────────────────────
|
||||
// ─── PedidoItem (C3/C4) ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Pedido emitido pelo Rep. Itens desnormalizados (produto sem FK — C4 traz catálogo).
|
||||
// number: gerado pelo SAR (sequencial por workspace, ex: "PED-00042").
|
||||
// discountPct: desconto global do pedido (além de descontos por item).
|
||||
// approvedById: userId de quem aprovou (se status = approved ou invoiced).
|
||||
// Item do pedido. Produto desnormalizado via idProduto (vw_produtos).
|
||||
|
||||
model Order {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
number String @unique // "PED-00001"
|
||||
clientId String @db.Uuid
|
||||
repId String // userId do Rep que emitiu
|
||||
status OrderStatus @default(budget)
|
||||
discountPct Decimal @default(0) @db.Decimal(5, 2) // % desconto global
|
||||
subtotal Decimal @db.Decimal(15, 2) // soma dos itens sem desconto global
|
||||
total Decimal @db.Decimal(15, 2) // subtotal × (1 - discountPct/100)
|
||||
notes String?
|
||||
approvedById String? // userId de quem aprovou
|
||||
approvedAt DateTime?
|
||||
invoicedAt DateTime?
|
||||
cancelledAt DateTime?
|
||||
model PedidoItem {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
idPedido String @db.Uuid @map("id_pedido")
|
||||
ordem Int
|
||||
idProduto Int @map("id_produto")
|
||||
codProduto String? @map("cod_produto")
|
||||
descProduto String? @map("desc_produto")
|
||||
qtd Decimal @db.Decimal(10, 3)
|
||||
precoUnitario Decimal @db.Decimal(15, 2) @map("preco_unitario")
|
||||
descontoPerc Decimal @default(0) @db.Decimal(5, 2) @map("desconto_perc")
|
||||
descontoValor Decimal @default(0) @db.Decimal(15, 2) @map("desconto_valor")
|
||||
precoPauta Decimal @default(0) @db.Decimal(15, 2) @map("preco_pauta")
|
||||
comissao Decimal @default(0) @db.Decimal(15, 2)
|
||||
vlFlex Decimal @default(0) @db.Decimal(15, 2) @map("vl_flex")
|
||||
precoComIpi Decimal @default(0) @db.Decimal(15, 2) @map("preco_com_ipi")
|
||||
vlIpi Decimal @default(0) @db.Decimal(15, 2) @map("vl_ipi")
|
||||
vlIcmsst Decimal @default(0) @db.Decimal(15, 2) @map("vl_icmsst")
|
||||
total Decimal @db.Decimal(15, 2)
|
||||
|
||||
// Idempotency key para lançamentos offline (C4, FR-4.12)
|
||||
idempotencyKey String? @unique
|
||||
pedido Pedido @relation(fields: [idPedido], references: [id], onDelete: Cascade)
|
||||
|
||||
issuedAt DateTime @default(now()) // data de emissão pelo Rep
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
client Client @relation(fields: [clientId], references: [id])
|
||||
items OrderItem[]
|
||||
history OrderStatusHistory[]
|
||||
|
||||
@@index([clientId])
|
||||
@@index([repId])
|
||||
@@index([status])
|
||||
@@index([issuedAt])
|
||||
@@index([number])
|
||||
@@index([deletedAt])
|
||||
@@index([idPedido])
|
||||
@@map("pedido_itens")
|
||||
}
|
||||
|
||||
// ─── OrderItem (C3/C4) ───────────────────────────────────────────────────────
|
||||
// ─── HistoricoPedido (C3) ────────────────────────────────────────────────────
|
||||
//
|
||||
// Item do pedido. Produto desnormalizado (nome/código/categoria) — snapshot no momento do pedido.
|
||||
// productCategory: usado para validação de alçada por linha no POST /orders.
|
||||
// discountPct: desconto por linha (além do desconto global do Order).
|
||||
// Registro imutável de cada transição de situa. changedBy = cod_vendedor do ator.
|
||||
|
||||
model OrderItem {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
orderId String @db.Uuid
|
||||
productCode String // código no ERP / catálogo
|
||||
productName String // desnormalizado para exibição offline
|
||||
productCategory String @default("geral") // desnormalizado para alçada por linha
|
||||
quantity Decimal @db.Decimal(10, 3)
|
||||
unitPrice Decimal @db.Decimal(15, 2)
|
||||
discountPct Decimal @default(0) @db.Decimal(5, 2)
|
||||
subtotal Decimal @db.Decimal(15, 2) // qty × unitPrice × (1 - discountPct/100)
|
||||
model HistoricoPedido {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
idPedido String @db.Uuid @map("id_pedido")
|
||||
situaAnterior Int? @map("situa_anterior")
|
||||
situaNova Int @map("situa_nova")
|
||||
changedBy Int @map("changed_by")
|
||||
nota String?
|
||||
changedAt DateTime @default(now()) @map("changed_at")
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
pedido Pedido @relation(fields: [idPedido], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([orderId])
|
||||
@@index([idPedido])
|
||||
@@map("historico_pedido")
|
||||
}
|
||||
|
||||
// ─── Product (C4) ────────────────────────────────────────────────────────────
|
||||
// ─── AlcadaDesconto (C4) ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Catálogo sincronizado da view do ERP (FR-4.4). Rep usa para montar pedido.
|
||||
// category: agrupa produtos por linha para validação de alçada por linha (OQ-2).
|
||||
// unitPrice/stock: snapshot da última sync (TTL 4h — FR-4.4 [ASSUMPTION]).
|
||||
// Produto inativo (active=false) não aparece no catálogo mas histórico de pedidos mantém referência.
|
||||
// Alçada de desconto por vendedor, empresa e grupo de produto.
|
||||
// codGrupo = 0 → limite global/default do rep.
|
||||
|
||||
model Product {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code String @unique
|
||||
name String
|
||||
description String?
|
||||
category String @default("geral")
|
||||
unitPrice Decimal @db.Decimal(15, 2)
|
||||
stock Decimal? @db.Decimal(10, 3)
|
||||
active Boolean @default(true)
|
||||
erpCode String?
|
||||
syncedAt DateTime?
|
||||
model AlcadaDesconto {
|
||||
codVendedor Int @map("cod_vendedor")
|
||||
idEmpresa Int @map("id_empresa")
|
||||
codGrupo Int @default(0) @map("cod_grupo")
|
||||
limitePerc Decimal @default(5) @db.Decimal(5, 2) @map("limite_perc")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@index([code])
|
||||
@@index([name])
|
||||
@@index([category])
|
||||
@@index([active])
|
||||
@@index([deletedAt])
|
||||
@@id([codVendedor, idEmpresa, codGrupo])
|
||||
@@index([codVendedor, idEmpresa])
|
||||
@@map("alcada_desconto")
|
||||
}
|
||||
|
||||
// ─── RepTarget (C7) ──────────────────────────────────────────────────────────
|
||||
// ─── MetaRepresentante (C7) ──────────────────────────────────────────────────
|
||||
//
|
||||
// Meta mensal e taxas de comissão por rep. Uma linha por rep/mês.
|
||||
// commissionRate: % aplicada sobre o total aprovado+faturado do mês.
|
||||
// flexRate: % bônus adicional quando atingido >= targetAmount.
|
||||
// Meta mensal e taxas de comissão por rep. Uma linha por rep/empresa/mês.
|
||||
|
||||
model RepTarget {
|
||||
repId String
|
||||
year Int
|
||||
month Int // 1–12
|
||||
targetAmount Decimal @db.Decimal(15, 2)
|
||||
commissionRate Decimal @default(3) @db.Decimal(5, 2)
|
||||
flexRate Decimal @default(1) @db.Decimal(5, 2)
|
||||
model MetaRepresentante {
|
||||
codVendedor Int @map("cod_vendedor")
|
||||
idEmpresa Int @map("id_empresa")
|
||||
ano Int
|
||||
mes Int
|
||||
metaValor Decimal @db.Decimal(15, 2) @map("meta_valor")
|
||||
taxaComissao Decimal @default(3) @db.Decimal(5, 2) @map("taxa_comissao")
|
||||
taxaFlex Decimal @default(1) @db.Decimal(5, 2) @map("taxa_flex")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([repId, year, month])
|
||||
@@index([repId])
|
||||
}
|
||||
|
||||
// ─── RepDiscountLimit (C4) ───────────────────────────────────────────────────
|
||||
//
|
||||
// Alçada de desconto por rep e por linha de produto (OQ-2 resolvida 2026-05-27).
|
||||
// category = "__default__" → limite global do rep (fallback quando linha não tem override).
|
||||
// Lookup: (repId, category) → se não encontrado → (repId, "__default__") → senão 5%.
|
||||
|
||||
model RepDiscountLimit {
|
||||
repId String
|
||||
category String // "__default__" para limite global; category string para override por linha
|
||||
limit Decimal @default(5) @db.Decimal(5, 2)
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([repId, category])
|
||||
@@index([repId])
|
||||
@@id([codVendedor, idEmpresa, ano, mes])
|
||||
@@index([codVendedor, idEmpresa])
|
||||
@@map("meta_representante")
|
||||
}
|
||||
|
||||
// ─── PushSubscription (C6) ───────────────────────────────────────────────────
|
||||
//
|
||||
// Subscription VAPID Web Push por usuário. endpoint é único por dispositivo/browser.
|
||||
// role desnormalizado do JWT para filtrar destinatários (notifySupervisors, notifyUser).
|
||||
// codVendedor desnormalizado do JWT para filtrar destinatários.
|
||||
|
||||
model PushSubscription {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String
|
||||
role String // 'rep' | 'supervisor' | 'manager' | 'admin'
|
||||
endpoint String @unique
|
||||
p256dh String
|
||||
auth String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
codVendedor Int? @map("cod_vendedor")
|
||||
idEmpresa Int @map("id_empresa")
|
||||
role String
|
||||
endpoint String @unique
|
||||
p256dh String
|
||||
auth String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([userId])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
// ─── OrderStatusHistory (C3) ─────────────────────────────────────────────────
|
||||
//
|
||||
// Registro imutável de cada transição de status. changedById = userId do ator.
|
||||
|
||||
model OrderStatusHistory {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
orderId String @db.Uuid
|
||||
fromStatus OrderStatus?
|
||||
toStatus OrderStatus
|
||||
changedById String // userId
|
||||
note String?
|
||||
changedAt DateTime @default(now())
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([orderId])
|
||||
@@index([changedAt])
|
||||
@@index([idEmpresa])
|
||||
@@index([codVendedor])
|
||||
@@map("push_subscription")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user