// SAR — Workspace Database Schema // Stack canon: Prisma 7 · PostgreSQL 18 · BD-por-workspace (ADR 0006) // // Este schema roda em CADA workspace DB (sar_workspace_). // 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. // // CODING-RULES PGD-DB-004: moduleFormat = "cjs" (NestJS é CJS) // CODING-RULES PGD-DB-001: MIGRATION_DATABASE_URL aponta direto ao PG (sem PgBouncer) generator client { provider = "prisma-client-js" output = "../../../node_modules/.prisma/client" moduleFormat = "cjs" } // Prisma 7: url removida do schema — conexão em prisma.config.ts (migrate) // e no WorkspacePrismaPool via PrismaPg adapter (runtime). 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) ───────────────────────────────────────────────────────────── // // 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. model Client { id String @id @default(uuid()) @db.Uuid name String tradeName String? taxId String @unique email String? phone String? address Json? financialStatus FinancialStatus @default(regular) creditLimit Decimal? @db.Decimal(15, 2) 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]) } // ─── Order (C3) ────────────────────────────────────────────────────────────── // // 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). 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? // Idempotency key para lançamentos offline (C4, FR-4.12) idempotencyKey String? @unique 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]) } // ─── OrderItem (C3/C4) ─────────────────────────────────────────────────────── // // 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). 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) order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) @@index([orderId]) } // ─── Product (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. 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? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? @@index([code]) @@index([name]) @@index([category]) @@index([active]) @@index([deletedAt]) } // ─── 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]) } // ─── 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]) }