metallkart-erp/tests/seed-test-data.ts

565 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* SPEC SEED-31: Seed test data for purchases workflow visual testing
* Run: npx tsx tests/seed-test-data.ts
* Idempotent: safe to run multiple times
*/
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('=== SEED-31: Creating test data for purchases workflow ===');
// --- 1. Client OZON ---
const ozon = await prisma.client.upsert({
where: { inn: '7704217370' },
update: {},
create: {
companyName: 'OZON',
inn: '7704217370',
abbreviation: 'OZ',
isActive: true,
},
});
console.log(`Client OZON: id=${ozon.id}`);
// --- 2. Suppliers ---
const allianceOpt = await prisma.supplier.upsert({
where: { inn: '7701234567' },
update: {},
create: {
companyName: 'ООО Компания Альянс Опт',
inn: '7701234567',
contactEmail: 'sales@alliance-opt.ru',
status: 'ACTIVE',
},
});
console.log(`Supplier Альянс Опт: id=${allianceOpt.id}`);
const korolevTube = await prisma.supplier.upsert({
where: { inn: '5001234567' },
update: {},
create: {
companyName: 'ООО Королевский трубный завод',
inn: '5001234567',
contactEmail: 'info@korolev-tube.ru',
status: 'ACTIVE',
},
});
console.log(`Supplier Королевский трубный завод: id=${korolevTube.id}`);
// KraskaProm already exists (id=3), but with different INN — find it
let kraskaProm = await prisma.supplier.findFirst({ where: { inn: '7702345678' } });
if (!kraskaProm) {
kraskaProm = await prisma.supplier.findFirst({ where: { companyName: { contains: 'КраскаПром' } } });
}
if (!kraskaProm) {
kraskaProm = await prisma.supplier.create({
data: {
companyName: 'ООО КраскаПром',
inn: '7702345678',
contactEmail: 'info@kraska-prom.ru',
status: 'ACTIVE',
},
});
}
console.log(`Supplier КраскаПром: id=${kraskaProm.id}`);
// TLT internal supplier — already exists (id=7), just find it
let tlt = await prisma.supplier.findFirst({ where: { isInternal: true } });
if (!tlt) {
tlt = await prisma.supplier.upsert({
where: { inn: '0000000000' },
update: {},
create: {
companyName: 'ТЛТ (interne)',
inn: '0000000000',
isInternal: true,
status: 'ACTIVE',
},
});
}
console.log(`Supplier TLT: id=${tlt.id}`);
// --- 3. Get admin user for createdById ---
const admin = await prisma.user.findFirst({ where: { role: 'ADMIN' } });
if (!admin) throw new Error('No ADMIN user found');
const acheteur = await prisma.user.findFirst({ where: { role: 'ACHETEUR' } });
if (!acheteur) throw new Error('No ACHETEUR user found');
const commercial = await prisma.user.findFirst({ where: { role: 'COMMERCIAL' } });
if (!commercial) throw new Error('No COMMERCIAL user found');
// --- 4. Order ---
// Check if we have a suitable LAUNCHED/IN_PRODUCTION order, or create one
let order = await prisma.order.findFirst({
where: { status: { in: ['LAUNCHED', 'IN_PRODUCTION'] }, quantity: { gte: 50 } },
orderBy: { id: 'desc' },
});
if (!order) {
// Create new order
order = await prisma.order.create({
data: {
orderCode: 'OZ2603РК-170',
productName: 'O32603OZ113-1730 - 4 Ролл-кейдж Нонсорт',
quantity: 170,
status: 'LAUNCHED',
clientId: ozon.id,
managerId: commercial!.id,
executionUnit: 'MAIN_PRODUCTION',
launchDate: new Date('2026-03-20'),
},
});
// Create status history
await prisma.orderStatusHistory.create({
data: {
orderId: order.id,
fromStatus: 'VALIDATED',
toStatus: 'LAUNCHED',
changedById: admin.id,
},
});
console.log(`Created order: id=${order.id}, code=${order.orderCode}`);
} else {
console.log(`Using existing order: id=${order.id}, code=${order.orderCode}, status=${order.status}`);
}
// --- 5. Purchase Requirements (9 PRs) ---
// Check if PRs already exist for this order with ORDERED status
const existingOrderedPRs = await prisma.purchaseRequirement.count({
where: { orderId: order.id, status: 'ORDERED' },
});
if (existingOrderedPRs >= 5) {
console.log(`PRs already exist (${existingOrderedPRs} ORDERED), skipping PR creation`);
} else {
// Clean existing PRs for this order if any incomplete set
const existingPRs = await prisma.purchaseRequirement.findMany({
where: { orderId: order.id },
select: { id: true },
});
if (existingPRs.length > 0 && existingPRs.length < 9) {
// Cleanup incomplete PRs (only if safe)
const unsafe = await prisma.purchaseRequirement.count({
where: { orderId: order.id, status: { in: ['IN_TENDER', 'ORDERED', 'RECEIVED'] } },
});
if (unsafe === 0) {
await prisma.purchaseRequirement.deleteMany({ where: { orderId: order.id } });
console.log('Cleaned incomplete PRs');
}
}
const prData = [
// LASER_FLAT pieces (TLT)
{ materialCategory: 'LASER_FLAT' as const, description: 'Крюк-пряжка', quantity: 170, unit: 'шт', estimatedPrice: 45, status: 'ORDERED' as const },
{ materialCategory: 'LASER_FLAT' as const, description: 'Кронштейн', quantity: 340, unit: 'шт', estimatedPrice: 28, status: 'ORDERED' as const },
{ materialCategory: 'LASER_FLAT' as const, description: 'Стяжка основания', quantity: 170, unit: 'шт', estimatedPrice: 62, status: 'ORDERED' as const },
// LASER_TUBE (TLT)
{ materialCategory: 'LASER_TUBE' as const, description: 'Труба профильная 40x40x3', quantity: 1000, unit: 'м', estimatedPrice: 540, status: 'ORDERED' as const },
// PAINT
{ materialCategory: 'PAINT' as const, description: 'Краска RAL-5002', quantity: 1000, unit: 'кг', estimatedPrice: 385, status: 'ORDERED' as const },
// METAL
{ materialCategory: 'METAL' as const, description: 'Труба 20x20x1.5', quantity: 500, unit: 'м', estimatedPrice: 320, status: 'ORDERED' as const },
{ materialCategory: 'METAL' as const, description: 'Труба 25x25x1.5', quantity: 300, unit: 'м', estimatedPrice: 380, status: 'ORDERED' as const },
// FASTENERS
{ materialCategory: 'FASTENERS' as const, description: 'Болт M10x30', quantity: 680, unit: 'шт', estimatedPrice: 12, status: 'ORDERED' as const },
{ materialCategory: 'FASTENERS' as const, description: 'Гайка M10', quantity: 680, unit: 'шт', estimatedPrice: 5, status: 'ORDERED' as const },
];
const createdPRs: Array<{ id: number; description: string; materialCategory: string }> = [];
for (const pr of prData) {
const existing = await prisma.purchaseRequirement.findFirst({
where: { orderId: order.id, description: pr.description, materialCategory: pr.materialCategory },
});
if (existing) {
// Update status if needed
if (existing.status !== 'ORDERED') {
await prisma.purchaseRequirement.update({
where: { id: existing.id },
data: { status: 'ORDERED' },
});
}
createdPRs.push({ id: existing.id, description: existing.description, materialCategory: existing.materialCategory as string });
} else {
const created = await prisma.purchaseRequirement.create({
data: {
orderId: order.id,
materialCategory: pr.materialCategory,
description: pr.description,
quantity: new Prisma.Decimal(pr.quantity),
unit: pr.unit,
estimatedPrice: new Prisma.Decimal(pr.estimatedPrice),
budgetAmount: new Prisma.Decimal(pr.quantity * pr.estimatedPrice),
status: pr.status,
priority: 'NORMAL',
sourceType: 'SMETA_IMPORT',
},
});
createdPRs.push({ id: created.id, description: created.description, materialCategory: created.materialCategory });
}
}
console.log(`PRs created/updated: ${createdPRs.length}`);
// --- 6. Tender AWARDED + Offers + LineAwards ---
let tender = await prisma.tender.findFirst({
where: { referenceCode: 'T-2603-007' },
});
if (!tender) {
tender = await prisma.tender.create({
data: {
referenceCode: 'T-2603-007',
status: 'AWARDED',
description: 'Тендер на материалы и покраску для Ролл-кейдж',
createdById: acheteur!.id,
deadline: new Date('2026-04-01'),
awardedAt: new Date('2026-03-22'),
},
});
console.log(`Created tender: id=${tender.id}, ref=${tender.referenceCode}`);
// Link non-TLT PRs to tender
const nonTltPRs = createdPRs.filter(pr =>
!['LASER_FLAT', 'LASER_TUBE', 'PIPE_CUT'].includes(pr.materialCategory)
);
for (const pr of nonTltPRs) {
await prisma.purchaseRequirement.update({
where: { id: pr.id },
data: { tenderId: tender.id },
});
}
// Create TenderOffers
const offerAlliance = await prisma.tenderOffer.create({
data: {
tenderId: tender.id,
supplierId: allianceOpt.id,
proposedPrice: new Prisma.Decimal(396560),
deliveryDays: 14,
conditions: '100% предоплата',
receivedVia: 'EMAIL',
},
});
const offerKorolev = await prisma.tenderOffer.create({
data: {
tenderId: tender.id,
supplierId: korolevTube.id,
proposedPrice: new Prisma.Decimal(274000),
deliveryDays: 21,
conditions: '50/50',
receivedVia: 'EMAIL',
},
});
// Create TenderOfferLines
// Alliance: paint + fasteners
const paintPR = createdPRs.find(p => p.description === 'Краска RAL-5002')!;
const bolt = createdPRs.find(p => p.description === 'Болт M10x30')!;
const nut = createdPRs.find(p => p.description === 'Гайка M10')!;
for (const { pr, price, qty } of [
{ pr: paintPR, price: 385, qty: 1000 },
{ pr: bolt, price: 12, qty: 680 },
{ pr: nut, price: 5, qty: 680 },
]) {
await prisma.tenderOfferLine.create({
data: {
offerId: offerAlliance.id,
requirementId: pr.id,
unitPrice: new Prisma.Decimal(price),
quantity: new Prisma.Decimal(qty),
totalLinePrice: new Prisma.Decimal(price * qty),
available: true,
},
});
}
// Korolev: metal tubes
const tube20 = createdPRs.find(p => p.description === 'Труба 20x20x1.5')!;
const tube25 = createdPRs.find(p => p.description === 'Труба 25x25x1.5')!;
for (const { pr, price, qty } of [
{ pr: tube20, price: 320, qty: 500 },
{ pr: tube25, price: 380, qty: 300 },
]) {
await prisma.tenderOfferLine.create({
data: {
offerId: offerKorolev.id,
requirementId: pr.id,
unitPrice: new Prisma.Decimal(price),
quantity: new Prisma.Decimal(qty),
totalLinePrice: new Prisma.Decimal(price * qty),
available: true,
},
});
}
// Create TenderLineAwards
// Alliance wins: paint + fasteners
for (const { pr, price, qty } of [
{ pr: paintPR, price: 385, qty: 1000 },
{ pr: bolt, price: 12, qty: 680 },
{ pr: nut, price: 5, qty: 680 },
]) {
await prisma.tenderLineAward.create({
data: {
tenderId: tender.id,
requirementId: pr.id,
offerId: offerAlliance.id,
supplierId: allianceOpt.id,
unitPrice: new Prisma.Decimal(price),
quantity: new Prisma.Decimal(qty),
totalPrice: new Prisma.Decimal(price * qty),
awardedById: acheteur!.id,
},
});
}
// Korolev wins: metal tubes
for (const { pr, price, qty } of [
{ pr: tube20, price: 320, qty: 500 },
{ pr: tube25, price: 380, qty: 300 },
]) {
await prisma.tenderLineAward.create({
data: {
tenderId: tender.id,
requirementId: pr.id,
offerId: offerKorolev.id,
supplierId: korolevTube.id,
unitPrice: new Prisma.Decimal(price),
quantity: new Prisma.Decimal(qty),
totalPrice: new Prisma.Decimal(price * qty),
awardedById: acheteur!.id,
},
});
}
console.log('Tender offers and awards created');
} else {
console.log(`Tender already exists: id=${tender.id}`);
}
// --- 7. Purchase Orders ---
// PO-2603-010: Alliance Opt — Paint
let po1 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2603-010' } });
if (!po1) {
po1 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2603-010',
supplierId: allianceOpt.id,
clientOrderId: order.id,
tenderId: tender.id,
items: [{ description: 'Краска RAL-5002', quantity: 1000, unit: 'кг', estimatedPrice: 385 }] as unknown as Prisma.InputJsonValue,
totalAmount: new Prisma.Decimal(385000),
status: 'CONFIRMED',
createdById: acheteur!.id,
expectedDeliveryDate: new Date('2026-04-10'),
},
});
// PO Lines
await prisma.purchaseOrderLine.create({
data: {
purchaseOrderId: po1.id,
description: 'Краска RAL-5002',
quantity: new Prisma.Decimal(1000),
unit: 'кг',
estimatedPrice: new Prisma.Decimal(385),
},
});
// PurchaseLot
const paintPR = createdPRs.find(p => p.description === 'Краска RAL-5002')!;
await prisma.purchaseLot.create({
data: {
requirementId: paintPR.id,
lotNumber: 1,
quantity: new Prisma.Decimal(1000),
deliveryDate: new Date('2026-04-10'),
status: 'ORDERED',
purchaseOrderId: po1.id,
},
});
// Payment schedule
const schedule1 = await prisma.pOPaymentSchedule.create({
data: {
purchaseOrderId: po1.id,
paymentType: 'PREPAYMENT_FULL',
},
});
await prisma.pOInstallment.create({
data: {
scheduleId: schedule1.id,
percentage: new Prisma.Decimal(100),
label: 'Предоплата',
sortOrder: 0,
},
});
console.log(`Created PO-2603-010: id=${po1.id}, CONFIRMED, 385 000₽`);
} else {
console.log(`PO-2603-010 already exists: id=${po1.id}`);
}
// PO-2603-011: Королевский трубный завод — Metal tubes
let po2 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2603-011' } });
if (!po2) {
const tube20PR = createdPRs.find(p => p.description === 'Труба 20x20x1.5')!;
const tube25PR = createdPRs.find(p => p.description === 'Труба 25x25x1.5')!;
const total = 500 * 320 + 300 * 380; // 160000 + 114000 = 274000
po2 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2603-011',
supplierId: korolevTube.id,
clientOrderId: order.id,
tenderId: tender.id,
items: [
{ description: 'Труба 20x20x1.5', quantity: 500, unit: 'м', estimatedPrice: 320 },
{ description: 'Труба 25x25x1.5', quantity: 300, unit: 'м', estimatedPrice: 380 },
] as unknown as Prisma.InputJsonValue,
totalAmount: new Prisma.Decimal(total),
status: 'DRAFT',
createdById: acheteur!.id,
expectedDeliveryDate: new Date('2026-04-20'),
},
});
// PO Lines
await prisma.purchaseOrderLine.createMany({
data: [
{ purchaseOrderId: po2.id, description: 'Труба 20x20x1.5', material: 'С245', quantity: new Prisma.Decimal(500), unit: 'м', estimatedPrice: new Prisma.Decimal(320) },
{ purchaseOrderId: po2.id, description: 'Труба 25x25x1.5', material: 'С245', quantity: new Prisma.Decimal(300), unit: 'м', estimatedPrice: new Prisma.Decimal(380) },
],
});
// PurchaseLots — 1 per item
await prisma.purchaseLot.create({
data: {
requirementId: tube20PR.id,
lotNumber: 1,
quantity: new Prisma.Decimal(500),
deliveryDate: new Date('2026-04-15'),
status: 'PLANNED',
purchaseOrderId: po2.id,
},
});
await prisma.purchaseLot.create({
data: {
requirementId: tube25PR.id,
lotNumber: 1,
quantity: new Prisma.Decimal(300),
deliveryDate: new Date('2026-04-20'),
status: 'PLANNED',
purchaseOrderId: po2.id,
},
});
console.log(`Created PO-2603-011: id=${po2.id}, DRAFT, ${total}`);
} else {
console.log(`PO-2603-011 already exists: id=${po2.id}`);
}
// PO-2603-012: Alliance Opt — Fasteners
let po3 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2603-012' } });
if (!po3) {
const boltPR = createdPRs.find(p => p.description === 'Болт M10x30')!;
const nutPR = createdPRs.find(p => p.description === 'Гайка M10')!;
const total = 680 * 12 + 680 * 5; // 8160 + 3400 = 11560
po3 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2603-012',
supplierId: allianceOpt.id,
clientOrderId: order.id,
tenderId: tender.id,
items: [
{ description: 'Болт M10x30', quantity: 680, unit: 'шт', estimatedPrice: 12 },
{ description: 'Гайка M10', quantity: 680, unit: 'шт', estimatedPrice: 5 },
] as unknown as Prisma.InputJsonValue,
totalAmount: new Prisma.Decimal(total),
status: 'DRAFT',
createdById: acheteur!.id,
expectedDeliveryDate: new Date('2026-04-05'),
},
});
// PO Lines
await prisma.purchaseOrderLine.createMany({
data: [
{ purchaseOrderId: po3.id, description: 'Болт M10x30', quantity: new Prisma.Decimal(680), unit: 'шт', estimatedPrice: new Prisma.Decimal(12) },
{ purchaseOrderId: po3.id, description: 'Гайка M10', quantity: new Prisma.Decimal(680), unit: 'шт', estimatedPrice: new Prisma.Decimal(5) },
],
});
// PurchaseLot
await prisma.purchaseLot.create({
data: {
requirementId: boltPR.id,
lotNumber: 1,
quantity: new Prisma.Decimal(680),
deliveryDate: new Date('2026-04-05'),
status: 'PLANNED',
purchaseOrderId: po3.id,
},
});
await prisma.purchaseLot.create({
data: {
requirementId: nutPR.id,
lotNumber: 1,
quantity: new Prisma.Decimal(680),
deliveryDate: new Date('2026-04-05'),
status: 'PLANNED',
purchaseOrderId: po3.id,
},
});
console.log(`Created PO-2603-012: id=${po3.id}, DRAFT, ${total}`);
} else {
console.log(`PO-2603-012 already exists: id=${po3.id}`);
}
// Link TenderLineAwards to POs
if (po1 && tender) {
const paintPR = createdPRs.find(p => p.description === 'Краска RAL-5002')!;
await prisma.tenderLineAward.updateMany({
where: { tenderId: tender.id, requirementId: paintPR.id },
data: { purchaseOrderId: po1.id },
});
}
if (po2 && tender) {
const tube20PR = createdPRs.find(p => p.description === 'Труба 20x20x1.5')!;
const tube25PR = createdPRs.find(p => p.description === 'Труба 25x25x1.5')!;
await prisma.tenderLineAward.updateMany({
where: { tenderId: tender.id, requirementId: { in: [tube20PR.id, tube25PR.id] } },
data: { purchaseOrderId: po2.id },
});
}
if (po3 && tender) {
const boltPR = createdPRs.find(p => p.description === 'Болт M10x30')!;
const nutPR = createdPRs.find(p => p.description === 'Гайка M10')!;
await prisma.tenderLineAward.updateMany({
where: { tenderId: tender.id, requirementId: { in: [boltPR.id, nutPR.id] } },
data: { purchaseOrderId: po3.id },
});
}
}
// --- Summary ---
const poCount = await prisma.purchaseOrder.count();
const prCount = await prisma.purchaseRequirement.count({ where: { orderId: order.id } });
const lotCount = await prisma.purchaseLot.count();
const tenderCount = await prisma.tender.count();
const awardCount = await prisma.tenderLineAward.count();
const scheduleCount = await prisma.pOPaymentSchedule.count();
console.log('\n=== Summary ===');
console.log(`Orders: ${(await prisma.order.count())}`);
console.log(`PRs for order ${order.orderCode}: ${prCount}`);
console.log(`Tenders: ${tenderCount}`);
console.log(`TenderLineAwards: ${awardCount}`);
console.log(`POs: ${poCount}`);
console.log(`PO Lots: ${lotCount}`);
console.log(`Payment Schedules: ${scheduleCount}`);
console.log('\n=== Seed complete ===');
}
main()
.catch((e) => {
console.error('Seed failed:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());