feat(c4): lançamento de pedido — catálogo, alçada por linha, POST /orders

- Prisma: Product + RepDiscountLimit + productCategory em OrderItem + migration
- Seed: 28 produtos (5 categorias) + alçadas user-001 (default 10%, bebidas 8%, perecíveis 5%)
- @sar/api-interface: ProductSummarySchema, ProductDetailSchema, ProductSyncRequestSchema, CreateOrderSchema
- API: CatalogModule (GET /catalog, GET /catalog/:id, POST /catalog/sync)
- API: POST /orders — valida alçada por linha/produto (OQ-2), idempotency-key (FR-4.3), desnorm cliente
- Web: NewOrderPage (3 steps: catálogo → desconto/obs → confirmação)
- Web: botão Novo Pedido na ClientDetailPage (desabilitado se financialStatus=blocked)
- Web: rota /pedidos/novo com search param clientId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 23:45:11 +00:00
parent c36451dd33
commit 6769a0d82a
16 changed files with 1372 additions and 17 deletions

View File

@@ -119,26 +119,75 @@ model Order {
@@index([deletedAt])
}
// ─── OrderItem (C3) ──────────────────────────────────────────────────────────
// ─── OrderItem (C3/C4) ───────────────────────────────────────────────────────
//
// Item do pedido. Produto desnormalizado (nome/código como string) — catálogo virá em 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
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)
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.