Files
sar/apps/api/prisma/seed.ts
julian 6769a0d82a 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>
2026-05-27 23:45:11 +00:00

1109 lines
26 KiB
TypeScript

// 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<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;
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<string, OrderSeed[]> = {
// 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.`);
// 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();
});