// Seed de desenvolvimento — popula sar_workspace_dev com dados fictícios. // Executado via: pnpm exec prisma db seed (apps/api/) // NUNCA rodar em staging/prod. import { PrismaClient, FinancialStatus, OrderStatus, type Prisma } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] ?? 'postgresql://sar:sar_dev_password@localhost:5432/sar_workspace_dev', max: 2, }); const adapter = new PrismaPg(pool); const prisma = new PrismaClient({ adapter }); const DEV_REP_ID = 'user-001'; const DEV_REP2_ID = 'user-002'; const APPROVER_ID = 'user-manager-01'; function daysAgo(days: number): Date { const d = new Date(); d.setDate(d.getDate() - days); return d; } function calcSubtotal(qty: number, price: number, discPct: number): number { return Math.round(qty * price * (1 - discPct / 100) * 100) / 100; } function orderNumber(n: number): string { return `PED-${String(n).padStart(5, '0')}`; } const clientDefs = [ { name: 'Padaria São João Ltda', tradeName: 'Padaria São João', taxId: '12345678000195', email: 'contato@padariasaojoao.com.br', phone: '(11) 3456-7890', address: { street: 'Rua das Flores', number: '123', district: 'Centro', city: 'São Paulo', state: 'SP', zip: '01310100', }, financialStatus: FinancialStatus.regular, creditLimit: 15000.0, repId: DEV_REP_ID, erpCode: 'CLI-001', }, { name: 'Supermercado Bom Preço Eireli', tradeName: 'Bom Preço', taxId: '98765432000187', email: 'compras@bompreco.com.br', phone: '(11) 4567-8901', address: { street: 'Av. Paulista', number: '900', district: 'Bela Vista', city: 'São Paulo', state: 'SP', zip: '01311100', }, financialStatus: FinancialStatus.regular, creditLimit: 50000.0, repId: DEV_REP_ID, erpCode: 'CLI-002', }, { name: 'Mercearia do Seu Zé ME', tradeName: 'Mercearia Zé', taxId: '11223344000156', email: null, phone: '(11) 9876-5432', address: { street: 'Rua Quinze de Novembro', number: '45', district: 'Vila Nova', city: 'Guarulhos', state: 'SP', zip: '07031070', }, financialStatus: FinancialStatus.attention, creditLimit: 5000.0, repId: DEV_REP_ID, erpCode: 'CLI-003', }, { name: 'Distribuidora Norte Alimentos SA', tradeName: 'Norte Alimentos', taxId: '55667788000143', email: 'pedidos@nortealimentos.com.br', phone: '(92) 3344-5566', address: { street: 'Av. Brasil', number: '2200', district: 'Industrial', city: 'Manaus', state: 'AM', zip: '69075001', }, financialStatus: FinancialStatus.regular, creditLimit: 120000.0, repId: DEV_REP_ID, erpCode: 'CLI-004', }, { name: 'Bar e Lanchonete do Carlos', tradeName: null, taxId: '33221100000178', email: null, phone: '(21) 99887-6655', address: { street: 'Rua da Alfândega', number: '12', district: 'Centro', city: 'Rio de Janeiro', state: 'RJ', zip: '20070002', }, financialStatus: FinancialStatus.blocked, creditLimit: 2000.0, repId: DEV_REP_ID, erpCode: 'CLI-005', }, { name: 'Restaurante Sabor da Terra Ltda', tradeName: 'Sabor da Terra', taxId: '77889900000132', email: 'admin@sabordaterra.com.br', phone: '(31) 3322-1100', address: { street: 'Rua dos Inconfidentes', number: '560', district: 'Savassi', city: 'Belo Horizonte', state: 'MG', zip: '30140128', }, financialStatus: FinancialStatus.regular, creditLimit: 30000.0, repId: DEV_REP_ID, erpCode: 'CLI-006', }, { name: 'Atacadão Central Comércio Ltda', tradeName: 'Atacadão Central', taxId: '44556677000119', email: 'compras@atacadaocentral.com.br', phone: '(51) 3288-9900', address: { street: 'Av. Assis Brasil', number: '3970', district: "Passo d'Areia", city: 'Porto Alegre', state: 'RS', zip: '91010003', }, financialStatus: FinancialStatus.regular, creditLimit: 80000.0, repId: DEV_REP_ID, erpCode: 'CLI-007', }, { name: 'Quitanda Boa Vista ME', tradeName: 'Quitanda Boa Vista', taxId: '22334455000167', email: null, phone: '(48) 3344-2211', address: { street: 'Rua Felipe Schmidt', number: '88', district: 'Centro', city: 'Florianópolis', state: 'SC', zip: '88010001', }, financialStatus: FinancialStatus.attention, creditLimit: 3500.0, repId: DEV_REP_ID, erpCode: 'CLI-008', }, { name: 'Empório Gourmet Curitiba Ltda', tradeName: 'Empório Gourmet', taxId: '66778899000124', email: 'pedidos@emporiogourmet.com.br', phone: '(41) 3233-4455', address: { street: 'Rua XV de Novembro', number: '700', district: 'Centro', city: 'Curitiba', state: 'PR', zip: '80060000', }, financialStatus: FinancialStatus.regular, creditLimit: 25000.0, repId: DEV_REP2_ID, erpCode: 'CLI-009', }, { name: 'Mini Mercado Esperança', tradeName: null, taxId: '88990011000135', email: null, phone: '(85) 9988-7766', address: { street: 'Av. Bezerra de Menezes', number: '1800', district: 'São Gerardo', city: 'Fortaleza', state: 'CE', zip: '60325001', }, financialStatus: FinancialStatus.regular, creditLimit: 8000.0, repId: DEV_REP2_ID, erpCode: 'CLI-010', }, ]; // ─── 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 = 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; issuedDaysAgo: number; discountPct: number; notes?: string; items: { productCode: string; productName: string; qty: number; unitPrice: number; discountPct: number; }[]; }; // 17 pedidos distribuídos por 7 clientes de user-001 const ordersByTaxId: Record = { // Padaria São João — 2 pedidos (invoiced + budget) '12345678000195': [ { num: 1, status: OrderStatus.invoiced, issuedDaysAgo: 20, discountPct: 0, items: [ { productCode: 'FAR-001', productName: 'Farinha de Trigo 25kg', qty: 10, unitPrice: 89.9, discountPct: 0, }, { productCode: 'ACU-001', productName: 'Açúcar Cristal 50kg', qty: 5, unitPrice: 145.0, discountPct: 0, }, ], }, { num: 2, status: OrderStatus.budget, issuedDaysAgo: 3, discountPct: 2, items: [ { productCode: 'FAR-001', productName: 'Farinha de Trigo 25kg', qty: 8, unitPrice: 89.9, discountPct: 0, }, { productCode: 'LEV-001', productName: 'Fermento Biológico 500g', qty: 20, unitPrice: 12.5, discountPct: 5, }, ], }, ], // Supermercado Bom Preço — 3 pedidos (invoiced + approved + pending_approval) '98765432000187': [ { num: 3, status: OrderStatus.invoiced, issuedDaysAgo: 45, discountPct: 3, items: [ { productCode: 'OLE-001', productName: 'Óleo de Soja 900ml cx18', qty: 20, unitPrice: 98.0, discountPct: 0, }, { productCode: 'ARR-001', productName: 'Arroz Tipo 1 5kg cx10', qty: 15, unitPrice: 75.0, discountPct: 2, }, { productCode: 'FEI-001', productName: 'Feijão Carioca 1kg cx10', qty: 10, unitPrice: 65.0, discountPct: 0, }, ], }, { num: 4, status: OrderStatus.pending_approval, issuedDaysAgo: 2, discountPct: 5, notes: 'Cliente solicitou desconto especial para reposição de estoque', items: [ { productCode: 'OLE-001', productName: 'Óleo de Soja 900ml cx18', qty: 30, unitPrice: 98.0, discountPct: 3, }, { productCode: 'ARR-001', productName: 'Arroz Tipo 1 5kg cx10', qty: 25, unitPrice: 75.0, discountPct: 3, }, ], }, { num: 5, status: OrderStatus.approved, issuedDaysAgo: 10, discountPct: 0, items: [ { productCode: 'SAB-001', productName: 'Sabão em Pó 1kg cx12', qty: 15, unitPrice: 42.0, discountPct: 0, }, { productCode: 'DET-001', productName: 'Detergente 500ml cx24', qty: 10, unitPrice: 38.4, discountPct: 0, }, ], }, ], // Mercearia Zé — 1 pedido (invoiced, há 35 dias — activityStatus alert) '11223344000156': [ { num: 6, status: OrderStatus.invoiced, issuedDaysAgo: 35, discountPct: 0, items: [ { productCode: 'REF-001', productName: 'Refrigerante 2L cx6', qty: 5, unitPrice: 42.0, discountPct: 0, }, { productCode: 'AGU-001', productName: 'Água Mineral 500ml cx12', qty: 8, unitPrice: 18.0, discountPct: 0, }, ], }, ], // Distribuidora Norte — 4 pedidos (2 invoiced + 1 approved + 1 pending_approval) '55667788000143': [ { num: 7, status: OrderStatus.invoiced, issuedDaysAgo: 60, discountPct: 2, items: [ { productCode: 'LEI-001', productName: 'Leite UHT Integral 1L cx12', qty: 50, unitPrice: 54.0, discountPct: 0, }, { productCode: 'QUE-001', productName: 'Queijo Mussarela kg', qty: 30, unitPrice: 38.0, discountPct: 5, }, ], }, { num: 8, status: OrderStatus.invoiced, issuedDaysAgo: 30, discountPct: 0, items: [ { productCode: 'EMB-001', productName: 'Embutidos Sortidos kg', qty: 40, unitPrice: 28.0, discountPct: 0, }, { productCode: 'LEI-001', productName: 'Leite UHT Integral 1L cx12', qty: 60, unitPrice: 54.0, discountPct: 2, }, ], }, { num: 9, status: OrderStatus.pending_approval, issuedDaysAgo: 1, discountPct: 8, notes: 'Grande volume — precisa aprovação de gerente', items: [ { productCode: 'LEI-001', productName: 'Leite UHT Integral 1L cx12', qty: 100, unitPrice: 54.0, discountPct: 5, }, { productCode: 'QUE-001', productName: 'Queijo Mussarela kg', qty: 80, unitPrice: 38.0, discountPct: 5, }, { productCode: 'EMB-001', productName: 'Embutidos Sortidos kg', qty: 60, unitPrice: 28.0, discountPct: 0, }, ], }, { num: 10, status: OrderStatus.approved, issuedDaysAgo: 5, discountPct: 3, items: [ { productCode: 'CON-001', productName: 'Conservas Sortidas cx24', qty: 20, unitPrice: 96.0, discountPct: 0, }, { productCode: 'MOL-001', productName: 'Molho de Tomate 340g cx24', qty: 15, unitPrice: 72.0, discountPct: 3, }, ], }, ], // Restaurante Sabor da Terra — 2 pedidos (invoiced + invoiced) '77889900000132': [ { num: 11, status: OrderStatus.invoiced, issuedDaysAgo: 40, discountPct: 0, items: [ { productCode: 'CAR-001', productName: 'Carne Bovina Contrafilé kg', qty: 20, unitPrice: 65.0, discountPct: 0, }, { productCode: 'FRA-001', productName: 'Frango Inteiro Resfriado kg', qty: 30, unitPrice: 18.5, discountPct: 0, }, ], }, { num: 12, status: OrderStatus.invoiced, issuedDaysAgo: 15, discountPct: 5, items: [ { productCode: 'CAR-001', productName: 'Carne Bovina Contrafilé kg', qty: 25, unitPrice: 65.0, discountPct: 0, }, { productCode: 'PEI-001', productName: 'Peixe Tilápia Filé kg', qty: 15, unitPrice: 32.0, discountPct: 0, }, { productCode: 'LEG-001', productName: 'Legumes Sortidos kg', qty: 20, unitPrice: 8.5, discountPct: 0, }, ], }, ], // Atacadão Central — 3 pedidos (invoiced + approved + pending_approval) '44556677000119': [ { num: 13, status: OrderStatus.invoiced, issuedDaysAgo: 50, discountPct: 4, items: [ { productCode: 'OLE-001', productName: 'Óleo de Soja 900ml cx18', qty: 60, unitPrice: 98.0, discountPct: 2, }, { productCode: 'ARR-001', productName: 'Arroz Tipo 1 5kg cx10', qty: 40, unitPrice: 75.0, discountPct: 3, }, { productCode: 'ACU-001', productName: 'Açúcar Cristal 50kg', qty: 15, unitPrice: 145.0, discountPct: 0, }, ], }, { num: 14, status: OrderStatus.invoiced, issuedDaysAgo: 20, discountPct: 3, items: [ { productCode: 'FEI-001', productName: 'Feijão Carioca 1kg cx10', qty: 50, unitPrice: 65.0, discountPct: 2, }, { productCode: 'LEI-001', productName: 'Leite UHT Integral 1L cx12', qty: 40, unitPrice: 54.0, discountPct: 0, }, ], }, { num: 15, status: OrderStatus.approved, issuedDaysAgo: 7, discountPct: 5, items: [ { productCode: 'HIG-001', productName: 'Shampoo 400ml cx12', qty: 30, unitPrice: 96.0, discountPct: 0, }, { productCode: 'SAB-001', productName: 'Sabão em Pó 1kg cx12', qty: 25, unitPrice: 42.0, discountPct: 5, }, ], }, ], // Quitanda Boa Vista — 2 pedidos (cancelled + budget) '22334455000167': [ { num: 16, status: OrderStatus.cancelled, issuedDaysAgo: 50, discountPct: 0, notes: 'Cliente cancelou por falta de espaço em estoque', items: [ { productCode: 'FRU-001', productName: 'Frutas Sortidas cx', qty: 10, unitPrice: 45.0, discountPct: 0, }, ], }, { num: 17, status: OrderStatus.pending_approval, issuedDaysAgo: 1, discountPct: 10, notes: 'Desconto acima do limite — aguardando aprovação', items: [ { productCode: 'FRU-001', productName: 'Frutas Sortidas cx', qty: 8, unitPrice: 45.0, discountPct: 10, }, { productCode: 'VER-001', productName: 'Verduras Sortidas kg', qty: 15, unitPrice: 6.0, discountPct: 0, }, ], }, ], }; function buildHistoryForStatus(status: OrderStatus, repId: string, issuedDaysAgo: number) { const entries: { fromStatus: OrderStatus | null; toStatus: OrderStatus; changedById: string; changedAt: Date; note?: string; }[] = []; entries.push({ fromStatus: null, toStatus: OrderStatus.budget, changedById: repId, changedAt: daysAgo(issuedDaysAgo), }); if (status === OrderStatus.budget) return entries; if (status === OrderStatus.cancelled) { entries.push({ fromStatus: OrderStatus.budget, toStatus: OrderStatus.cancelled, changedById: repId, changedAt: daysAgo(issuedDaysAgo - 1), }); return entries; } entries.push({ fromStatus: OrderStatus.budget, toStatus: OrderStatus.pending_approval, changedById: repId, changedAt: daysAgo(issuedDaysAgo - 0.1), }); if (status === OrderStatus.pending_approval) return entries; entries.push({ fromStatus: OrderStatus.pending_approval, toStatus: OrderStatus.approved, changedById: APPROVER_ID, changedAt: daysAgo(issuedDaysAgo - 0.5), }); if (status === OrderStatus.approved) return entries; entries.push({ fromStatus: OrderStatus.approved, toStatus: OrderStatus.invoiced, changedById: APPROVER_ID, changedAt: daysAgo(issuedDaysAgo - 1), }); return entries; } 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.`); // Metas mensais (C7) const now = new Date(); const repTargets = [ { repId: DEV_REP_ID, year: now.getFullYear(), month: now.getMonth() + 1, targetAmount: 60000, commissionRate: 3, flexRate: 1, }, { repId: DEV_REP2_ID, year: now.getFullYear(), month: now.getMonth() + 1, targetAmount: 40000, commissionRate: 3, flexRate: 1, }, ]; for (const t of repTargets) { await prisma.repTarget.upsert({ where: { repId_year_month: { repId: t.repId, year: t.year, month: t.month } }, create: t, update: { targetAmount: t.targetAmount }, }); } console.log(`${repTargets.length} metas mensais configuradas.`); // Upsert clients (sem lastOrderAt/openOrdersCount — calculados depois) for (const data of clientDefs) { await prisma.client.upsert({ where: { taxId: data.taxId }, create: { ...data, syncedAt: new Date() }, update: { name: data.name, financialStatus: data.financialStatus, creditLimit: data.creditLimit, syncedAt: new Date(), }, }); } console.log(`${clientDefs.length} clientes upserted.`); // Delete existing orders (re-seed idempotente) await prisma.orderStatusHistory.deleteMany({}); await prisma.orderItem.deleteMany({}); await prisma.order.deleteMany({}); console.log('Pedidos anteriores removidos.'); let orderCount = 0; for (const [taxId, orders] of Object.entries(ordersByTaxId)) { const client = await prisma.client.findUniqueOrThrow({ where: { taxId } }); for (const o of orders) { const issuedAt = daysAgo(o.issuedDaysAgo); // 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, subtotal: calcSubtotal(it.qty, it.unitPrice, it.discountPct), })); const itemsSubtotal = itemsData.reduce((acc, it) => acc + Number(it.subtotal), 0); const orderTotal = Math.round(itemsSubtotal * (1 - o.discountPct / 100) * 100) / 100; const historyEntries = buildHistoryForStatus(o.status, client.repId, o.issuedDaysAgo); const approvedEntry = historyEntries.find((h) => h.toStatus === OrderStatus.approved); const invoicedEntry = historyEntries.find((h) => h.toStatus === OrderStatus.invoiced); const cancelledEntry = historyEntries.find((h) => h.toStatus === OrderStatus.cancelled); await prisma.order.create({ data: { number: orderNumber(o.num), clientId: client.id, repId: client.repId, status: o.status, discountPct: o.discountPct, subtotal: itemsSubtotal, total: orderTotal, notes: o.notes ?? null, approvedById: approvedEntry ? APPROVER_ID : null, approvedAt: approvedEntry?.changedAt ?? null, invoicedAt: invoicedEntry?.changedAt ?? null, cancelledAt: cancelledEntry?.changedAt ?? null, issuedAt, items: { create: itemsData }, history: { create: historyEntries.map((h) => ({ fromStatus: h.fromStatus ?? null, toStatus: h.toStatus, changedById: h.changedById, changedAt: h.changedAt, note: h.note ?? null, })), }, }, }); orderCount++; } } console.log(`${orderCount} pedidos criados.`); // Atualiza desnorm de clientes a partir dos pedidos criados for (const data of clientDefs) { const client = await prisma.client.findUniqueOrThrow({ where: { taxId: data.taxId } }); const orders = await prisma.order.findMany({ where: { clientId: client.id, deletedAt: null, status: { not: OrderStatus.cancelled } }, orderBy: { issuedAt: 'desc' }, }); const openStatuses: OrderStatus[] = [ OrderStatus.budget, OrderStatus.pending_approval, OrderStatus.approved, ]; const openCount = orders.filter((o) => openStatuses.includes(o.status)).length; const lastOrder = orders[0]; await prisma.client.update({ where: { id: client.id }, data: { lastOrderAt: lastOrder?.issuedAt ?? null, lastOrderValue: lastOrder ? lastOrder.total : null, openOrdersCount: openCount, }, }); } console.log('Desnorm de clientes atualizada.'); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); await pool.end(); });