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:
@@ -2,7 +2,7 @@
|
||||
// Executado via: pnpm exec prisma db seed (apps/api/)
|
||||
// NUNCA rodar em staging/prod.
|
||||
|
||||
import { PrismaClient, FinancialStatus, OrderStatus } from '@prisma/client';
|
||||
import { PrismaClient, FinancialStatus, OrderStatus, type Prisma } from '@prisma/client';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import pg from 'pg';
|
||||
|
||||
@@ -226,6 +226,253 @@ const clientDefs = [
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Catálogo de produtos (25 produtos, 5 categorias) ────────────────────────
|
||||
|
||||
const products: Prisma.ProductCreateInput[] = [
|
||||
// grãos
|
||||
{
|
||||
code: 'ARR-001',
|
||||
name: 'Arroz Tipo 1 5kg cx10',
|
||||
category: 'grãos',
|
||||
unitPrice: 75.0,
|
||||
stock: 500,
|
||||
erpCode: 'P0001',
|
||||
},
|
||||
{
|
||||
code: 'FEI-001',
|
||||
name: 'Feijão Carioca 1kg cx10',
|
||||
category: 'grãos',
|
||||
unitPrice: 65.0,
|
||||
stock: 400,
|
||||
erpCode: 'P0002',
|
||||
},
|
||||
{
|
||||
code: 'FAR-001',
|
||||
name: 'Farinha de Trigo 25kg',
|
||||
category: 'grãos',
|
||||
unitPrice: 89.9,
|
||||
stock: 200,
|
||||
erpCode: 'P0003',
|
||||
},
|
||||
{
|
||||
code: 'ACU-001',
|
||||
name: 'Açúcar Cristal 50kg',
|
||||
category: 'grãos',
|
||||
unitPrice: 145.0,
|
||||
stock: 150,
|
||||
erpCode: 'P0004',
|
||||
},
|
||||
{
|
||||
code: 'MIL-001',
|
||||
name: 'Flocão de Milho 500g cx20',
|
||||
category: 'grãos',
|
||||
unitPrice: 48.0,
|
||||
stock: 300,
|
||||
erpCode: 'P0005',
|
||||
},
|
||||
// bebidas
|
||||
{
|
||||
code: 'OLE-001',
|
||||
name: 'Óleo de Soja 900ml cx18',
|
||||
category: 'bebidas',
|
||||
unitPrice: 98.0,
|
||||
stock: 600,
|
||||
erpCode: 'P0006',
|
||||
},
|
||||
{
|
||||
code: 'REF-001',
|
||||
name: 'Refrigerante 2L cx6',
|
||||
category: 'bebidas',
|
||||
unitPrice: 42.0,
|
||||
stock: 800,
|
||||
erpCode: 'P0007',
|
||||
},
|
||||
{
|
||||
code: 'AGU-001',
|
||||
name: 'Água Mineral 500ml cx12',
|
||||
category: 'bebidas',
|
||||
unitPrice: 18.0,
|
||||
stock: 1000,
|
||||
erpCode: 'P0008',
|
||||
},
|
||||
{
|
||||
code: 'SUC-001',
|
||||
name: 'Suco de Caixinha 200ml cx27',
|
||||
category: 'bebidas',
|
||||
unitPrice: 32.4,
|
||||
stock: 700,
|
||||
erpCode: 'P0009',
|
||||
},
|
||||
{
|
||||
code: 'CER-001',
|
||||
name: 'Cerveja Lata 350ml cx12',
|
||||
category: 'bebidas',
|
||||
unitPrice: 52.8,
|
||||
stock: 400,
|
||||
erpCode: 'P0010',
|
||||
},
|
||||
// laticínios
|
||||
{
|
||||
code: 'LEI-001',
|
||||
name: 'Leite UHT Integral 1L cx12',
|
||||
category: 'laticínios',
|
||||
unitPrice: 54.0,
|
||||
stock: 900,
|
||||
erpCode: 'P0011',
|
||||
},
|
||||
{
|
||||
code: 'QUE-001',
|
||||
name: 'Queijo Mussarela kg',
|
||||
category: 'laticínios',
|
||||
unitPrice: 38.0,
|
||||
stock: 200,
|
||||
erpCode: 'P0012',
|
||||
},
|
||||
{
|
||||
code: 'IOG-001',
|
||||
name: 'Iogurte Natural 170g cx12',
|
||||
category: 'laticínios',
|
||||
unitPrice: 28.8,
|
||||
stock: 350,
|
||||
erpCode: 'P0013',
|
||||
},
|
||||
{
|
||||
code: 'MAN-001',
|
||||
name: 'Manteiga com Sal 200g cx12',
|
||||
category: 'laticínios',
|
||||
unitPrice: 84.0,
|
||||
stock: 250,
|
||||
erpCode: 'P0014',
|
||||
},
|
||||
{
|
||||
code: 'REQ-001',
|
||||
name: 'Requeijão Cremoso 200g cx12',
|
||||
category: 'laticínios',
|
||||
unitPrice: 72.0,
|
||||
stock: 180,
|
||||
erpCode: 'P0015',
|
||||
},
|
||||
// perecíveis
|
||||
{
|
||||
code: 'CAR-001',
|
||||
name: 'Carne Bovina Contrafilé kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 65.0,
|
||||
stock: 150,
|
||||
erpCode: 'P0016',
|
||||
},
|
||||
{
|
||||
code: 'FRA-001',
|
||||
name: 'Frango Inteiro Resfriado kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 18.5,
|
||||
stock: 400,
|
||||
erpCode: 'P0017',
|
||||
},
|
||||
{
|
||||
code: 'PEI-001',
|
||||
name: 'Peixe Tilápia Filé kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 32.0,
|
||||
stock: 100,
|
||||
erpCode: 'P0018',
|
||||
},
|
||||
{
|
||||
code: 'EMB-001',
|
||||
name: 'Embutidos Sortidos kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 28.0,
|
||||
stock: 300,
|
||||
erpCode: 'P0019',
|
||||
},
|
||||
{
|
||||
code: 'LEG-001',
|
||||
name: 'Legumes Sortidos kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 8.5,
|
||||
stock: 500,
|
||||
erpCode: 'P0020',
|
||||
},
|
||||
// higiene
|
||||
{
|
||||
code: 'SAB-001',
|
||||
name: 'Sabão em Pó 1kg cx12',
|
||||
category: 'higiene',
|
||||
unitPrice: 42.0,
|
||||
stock: 400,
|
||||
erpCode: 'P0021',
|
||||
},
|
||||
{
|
||||
code: 'DET-001',
|
||||
name: 'Detergente 500ml cx24',
|
||||
category: 'higiene',
|
||||
unitPrice: 38.4,
|
||||
stock: 600,
|
||||
erpCode: 'P0022',
|
||||
},
|
||||
{
|
||||
code: 'HIG-001',
|
||||
name: 'Shampoo 400ml cx12',
|
||||
category: 'higiene',
|
||||
unitPrice: 96.0,
|
||||
stock: 250,
|
||||
erpCode: 'P0023',
|
||||
},
|
||||
{
|
||||
code: 'LEV-001',
|
||||
name: 'Fermento Biológico 500g',
|
||||
category: 'grãos',
|
||||
unitPrice: 12.5,
|
||||
stock: 200,
|
||||
erpCode: 'P0024',
|
||||
},
|
||||
{
|
||||
code: 'CON-001',
|
||||
name: 'Conservas Sortidas cx24',
|
||||
category: 'grãos',
|
||||
unitPrice: 96.0,
|
||||
stock: 180,
|
||||
erpCode: 'P0025',
|
||||
},
|
||||
{
|
||||
code: 'MOL-001',
|
||||
name: 'Molho de Tomate 340g cx24',
|
||||
category: 'grãos',
|
||||
unitPrice: 72.0,
|
||||
stock: 220,
|
||||
erpCode: 'P0026',
|
||||
},
|
||||
{
|
||||
code: 'FRU-001',
|
||||
name: 'Frutas Sortidas cx',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 45.0,
|
||||
stock: 80,
|
||||
erpCode: 'P0027',
|
||||
},
|
||||
{
|
||||
code: 'VER-001',
|
||||
name: 'Verduras Sortidas kg',
|
||||
category: 'perecíveis',
|
||||
unitPrice: 6.0,
|
||||
stock: 300,
|
||||
erpCode: 'P0028',
|
||||
},
|
||||
];
|
||||
|
||||
// Mapa code → category para popular productCategory nos OrderItems do seed C3
|
||||
const productCategoryMap: Record<string, string> = Object.fromEntries(
|
||||
products.map((p) => [p.code, p.category ?? 'geral']),
|
||||
);
|
||||
|
||||
// Alçadas de desconto de user-001: default 10%, bebidas 8%, perecíveis 5%
|
||||
const repDiscountLimits = [
|
||||
{ repId: DEV_REP_ID, category: '__default__', limit: 10 },
|
||||
{ repId: DEV_REP_ID, category: 'bebidas', limit: 8 },
|
||||
{ repId: DEV_REP_ID, category: 'perecíveis', limit: 5 },
|
||||
{ repId: DEV_REP2_ID, category: '__default__', limit: 5 },
|
||||
];
|
||||
|
||||
type OrderSeed = {
|
||||
num: number;
|
||||
status: OrderStatus;
|
||||
@@ -721,6 +968,26 @@ function buildHistoryForStatus(status: OrderStatus, repId: string, issuedDaysAgo
|
||||
async function main() {
|
||||
console.log('Seed iniciado...');
|
||||
|
||||
// Upsert catálogo de produtos
|
||||
for (const p of products) {
|
||||
await prisma.product.upsert({
|
||||
where: { code: p.code },
|
||||
create: { ...p, syncedAt: new Date() },
|
||||
update: { name: p.name, unitPrice: p.unitPrice, stock: p.stock, syncedAt: new Date() },
|
||||
});
|
||||
}
|
||||
console.log(`${products.length} produtos upserted.`);
|
||||
|
||||
// Upsert alçadas de desconto
|
||||
for (const r of repDiscountLimits) {
|
||||
await prisma.repDiscountLimit.upsert({
|
||||
where: { repId_category: { repId: r.repId, category: r.category } },
|
||||
create: r,
|
||||
update: { limit: r.limit },
|
||||
});
|
||||
}
|
||||
console.log(`${repDiscountLimits.length} alçadas configuradas.`);
|
||||
|
||||
// Upsert clients (sem lastOrderAt/openOrdersCount — calculados depois)
|
||||
for (const data of clientDefs) {
|
||||
await prisma.client.upsert({
|
||||
@@ -749,10 +1016,11 @@ async function main() {
|
||||
for (const o of orders) {
|
||||
const issuedAt = daysAgo(o.issuedDaysAgo);
|
||||
|
||||
// Build items with subtotals
|
||||
// Build items with subtotals + productCategory (desnorm do catálogo)
|
||||
const itemsData = o.items.map((it) => ({
|
||||
productCode: it.productCode,
|
||||
productName: it.productName,
|
||||
productCategory: productCategoryMap[it.productCode] ?? 'geral',
|
||||
quantity: it.qty,
|
||||
unitPrice: it.unitPrice,
|
||||
discountPct: it.discountPct,
|
||||
|
||||
Reference in New Issue
Block a user