metallkart-erp/scripts/seed-beta-demo-data.ts
louis f1d9d88526 feat(seed): BETA-SEED-S107 — bootstrap scripts for VPS BETA opening
- seed-beta-users.ts: 7 users (1 per ERP role), bcrypt 12, idempotent by email
- seed-beta-demo-data.ts: 2 clients + 2 suppliers + 3 products ACTIVE (smeta LOCKED + 10 pricings)
- ADM_GENERAL absent from Role enum → mapped to USER
- All ДЕМО_ prefixed for visual identification
- Login test 7/7 PASS, re-run safe (password NOT overwritten)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 17:38:16 +04:00

340 lines
13 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.

import { PrismaClient, MaterialCategory, ProductStatus, SmetaLineType } from '@prisma/client';
const prisma = new PrismaClient();
// ──────────────────────────────────────────────────────────────
// DEMO CLIENTS
// ──────────────────────────────────────────────────────────────
const DEMO_CLIENTS = [
{
companyName: 'ДЕМО ООО Логистика',
inn: '7700000001',
kpp: '770000001',
abbreviation: ЕМО_CLI_001',
contactName: 'Иванов Пётр Сергеевич',
phone: '+7-495-000-00-01',
email: 'demo-cli-1@example.com',
address: 'ДЕМО адрес 1, г. Москва, ул. Складская, д. 1',
},
{
companyName: 'ДЕМО АО Складское оборудование',
inn: '7700099002',
kpp: '770099002',
abbreviation: ЕМО_CLI_002',
contactName: 'Петрова Мария Ивановна',
phone: '+7-495-000-00-02',
email: 'demo-cli-2@example.com',
address: 'ДЕМО адрес 2, г. Москва, пр-т Ленина, д. 10',
},
];
// ──────────────────────────────────────────────────────────────
// DEMO SUPPLIERS
// ──────────────────────────────────────────────────────────────
const DEMO_SUPPLIERS = [
{
companyName: 'ДЕМО ООО Металлторг',
inn: '7800000001',
kpp: '780000001',
legalAddress: 'ДЕМО адрес, г. Санкт-Петербург, ул. Металлургов, д. 5',
contactName: 'Сидоров Андрей Петрович',
contactPhone: '+7-812-000-00-01',
contactEmail: 'demo-sup-1@example.com',
productTags: ['METAL', 'FASTENERS', 'COMPONENTS'],
status: 'ACTIVE' as const,
},
{
companyName: 'ДЕМО ООО Метизы',
inn: '7800000002',
kpp: '780000002',
legalAddress: 'ДЕМО адрес, г. Санкт-Петербург, ул. Заводская, д. 12',
contactName: 'Козлова Елена Викторовна',
contactPhone: '+7-812-000-00-02',
contactEmail: 'demo-sup-2@example.com',
productTags: ['METAL', 'PAINT'],
status: 'ACTIVE' as const,
},
];
// ──────────────────────────────────────────────────────────────
// DEMO PRODUCTS + SMETA LINES + PRICINGS
// ──────────────────────────────────────────────────────────────
interface DemoSmetaLine {
description: string;
lineType: SmetaLineType;
lineCategory: string | null;
unit: string;
quantity: number;
unitPrice: number;
position: number;
}
interface DemoProduct {
code: string;
name: string;
description: string;
defaultQuantity: number;
dimensions: string;
massKg: number;
smetaLines: DemoSmetaLine[];
}
const DEMO_PRODUCTS: DemoProduct[] = [
{
code: ЕМО_PROD_001',
name: 'ДЕМО Стеллаж демо',
description: 'Демонстрационный стеллаж для складского хранения',
defaultQuantity: 1,
dimensions: '2000×1000×500',
massKg: 45.0,
smetaLines: [
{ description: 'Труба профильная 40×40×2', lineType: 'MATERIAL', lineCategory: 'METAL', unit: 'м', quantity: 12, unitPrice: 350, position: 1 },
{ description: 'Лист стальной 2мм', lineType: 'MATERIAL', lineCategory: 'METAL', unit: 'кг', quantity: 8, unitPrice: 65, position: 2 },
{ description: 'Болт M8×30', lineType: 'MATERIAL', lineCategory: 'FASTENERS', unit: 'шт', quantity: 24, unitPrice: 12, position: 3 },
{ description: 'Краска порошковая RAL7035', lineType: 'MATERIAL', lineCategory: 'PAINT', unit: 'кг', quantity: 0.5, unitPrice: 580, position: 4 },
],
},
{
code: ЕМО_PROD_002',
name: 'ДЕМО Тележка демо',
description: 'Демонстрационная промышленная тележка',
defaultQuantity: 1,
dimensions: '1200×600×900',
massKg: 28.0,
smetaLines: [
{ description: 'Труба 25×25×1.5', lineType: 'MATERIAL', lineCategory: 'METAL', unit: 'м', quantity: 6, unitPrice: 230, position: 1 },
{ description: 'Колесо поворотное 100мм', lineType: 'MATERIAL', lineCategory: 'COMPONENTS', unit: 'шт', quantity: 4, unitPrice: 450, position: 2 },
{ description: 'Болт M6×20', lineType: 'MATERIAL', lineCategory: 'FASTENERS', unit: 'шт', quantity: 16, unitPrice: 8, position: 3 },
],
},
{
code: ЕМО_PROD_003',
name: 'ДЕМО Контейнер демо',
description: 'Демонстрационный металлический контейнер для хранения',
defaultQuantity: 1,
dimensions: '800×600×600',
massKg: 18.5,
smetaLines: [
{ description: 'Лист стальной 1.5мм', lineType: 'MATERIAL', lineCategory: 'METAL', unit: 'кг', quantity: 12, unitPrice: 65, position: 1 },
{ description: 'Уголок 25×25×3', lineType: 'MATERIAL', lineCategory: 'METAL', unit: 'м', quantity: 4, unitPrice: 145, position: 2 },
{ description: 'Краска RAL5005', lineType: 'MATERIAL', lineCategory: 'PAINT', unit: 'кг', quantity: 0.3, unitPrice: 580, position: 3 },
],
},
];
async function getAdminUserId(): Promise<string> {
const admin = await prisma.user.findFirst({
where: { role: 'ADMIN', active: true },
select: { id: true },
});
if (!admin) throw new Error('No ADMIN user found — run seed-beta-users.ts first');
return admin.id;
}
async function seedClients() {
console.log('\n=== CLIENTS DEMO ===');
for (const c of DEMO_CLIENTS) {
const existing = await prisma.client.findUnique({ where: { inn: c.inn } });
if (existing) {
console.log(` EXISTS ${c.abbreviation} (inn=${c.inn})`);
continue;
}
await prisma.client.create({ data: c });
console.log(` CREATED ${c.abbreviation}${c.companyName}`);
}
}
async function seedSuppliers() {
console.log('\n=== SUPPLIERS DEMO ===');
const supplierIds: Record<string, number> = {};
for (const s of DEMO_SUPPLIERS) {
let supplier = await prisma.supplier.findUnique({ where: { inn: s.inn } });
if (supplier) {
console.log(` EXISTS ${s.companyName} (inn=${s.inn})`);
supplierIds[s.inn] = supplier.id;
continue;
}
supplier = await prisma.supplier.create({ data: s });
console.log(` CREATED ${s.companyName}`);
supplierIds[s.inn] = supplier.id;
}
return supplierIds;
}
async function seedProducts(adminUserId: string, supplierIds: Record<string, number>) {
console.log('\n=== PRODUCTS DEMO ===');
const sup1Id = supplierIds['7800000001'];
const sup2Id = supplierIds['7800000002'];
if (!sup1Id || !sup2Id) {
throw new Error('Demo suppliers not found — seedSuppliers must run first');
}
for (const dp of DEMO_PRODUCTS) {
const existing = await prisma.product.findUnique({ where: { code: dp.code } });
if (existing) {
console.log(` EXISTS ${dp.code} (id=${existing.id}, status=${existing.status})`);
continue;
}
const product = await prisma.product.create({
data: {
code: dp.code,
name: dp.name,
description: dp.description,
status: 'ACTIVE',
defaultQuantity: dp.defaultQuantity,
dimensions: dp.dimensions,
massKg: dp.massKg,
createdById: adminUserId,
},
});
console.log(` CREATED Product ${dp.code} (id=${product.id})`);
const totalMaterials = dp.smetaLines.reduce((sum, l) => sum + l.quantity * l.unitPrice, 0);
const smeta = await prisma.smeta.create({
data: {
productId: product.id,
version: 1,
status: 'LOCKED',
isLocked: true,
lockedAt: new Date(),
totalMaterials,
sellingPrice: totalMaterials * 1.3,
},
});
console.log(` Smeta id=${smeta.id} (LOCKED, totalMaterials=${totalMaterials})`);
const createdLines: { id: number; lineCategory: string | null; description: string }[] = [];
for (const sl of dp.smetaLines) {
const line = await prisma.smetaLine.create({
data: {
smetaId: smeta.id,
lineType: sl.lineType,
lineCategory: sl.lineCategory,
description: sl.description,
unit: sl.unit,
quantity: sl.quantity,
unitPrice: sl.unitPrice,
totalPrice: sl.quantity * sl.unitPrice,
position: sl.position,
},
});
createdLines.push({ id: line.id, lineCategory: sl.lineCategory, description: sl.description });
}
console.log(` ${createdLines.length} smeta lines created`);
let pricingsCreated = 0;
for (const line of createdLines) {
if (!line.lineCategory) continue;
const supplierId = ['PAINT'].includes(line.lineCategory) ? sup2Id : sup1Id;
const existingPricing = await prisma.productMaterialPricing.findUnique({
where: { smetaLineId: line.id },
});
if (existingPricing) continue;
const matchingSmetaLine = dp.smetaLines.find(sl => sl.description === line.description);
const unitPrice = matchingSmetaLine?.unitPrice ?? 100;
await prisma.productMaterialPricing.create({
data: {
productId: product.id,
smetaLineId: line.id,
supplierId,
currentUnitPrice: unitPrice,
currentDeliveryDays: 14,
status: 'ACTIVE',
createdById: adminUserId,
},
});
pricingsCreated++;
}
console.log(` ${pricingsCreated} pricings ACTIVE created`);
}
}
async function verifyReferentials() {
console.log('\n=== REFERENTIALS CHECK ===');
const expenseCodesCount = await prisma.expenseCode.count();
console.log(` expense_codes: ${expenseCodesCount}`);
if (expenseCodesCount === 0) {
console.warn(' ⚠ expense_codes is EMPTY — run: npx tsx src/scripts/seed-expense-codes.ts');
}
const smetaMappingsCount = await prisma.smetaBudgetMapping.count();
console.log(` smeta_budget_mappings: ${smetaMappingsCount}`);
if (smetaMappingsCount === 0) {
console.warn(' ⚠ smeta_budget_mappings is EMPTY — run: npx tsx src/scripts/seed-smeta-mapping.ts');
}
const helpCount = await prisma.helpArticle.count({ where: { isPublished: true } });
console.log(` help_articles (published): ${helpCount}`);
if (helpCount < 50) {
console.warn(' ⚠ help_articles < 50 — run: npx tsx scripts/seed-help.ts');
}
const notifConfigCount = await prisma.notificationCategoryConfig.count();
console.log(` notification_category_configs: ${notifConfigCount}`);
if (notifConfigCount < 10) {
console.warn(' ⚠ notification_category_configs < 10 — run: npx tsx scripts/seed-notification-configs.ts');
}
}
async function printSummary() {
console.log('\n=== SUMMARY ===');
const clients = await prisma.client.findMany({
where: { abbreviation: { startsWith: ЕМО_CLI_' } },
select: { abbreviation: true, companyName: true },
});
console.log(` Demo Clients: ${clients.length}`);
clients.forEach(c => console.log(` ${c.abbreviation}${c.companyName}`));
const suppliers = await prisma.supplier.findMany({
where: { inn: { in: DEMO_SUPPLIERS.map(s => s.inn) } },
select: { inn: true, companyName: true, productTags: true },
});
console.log(` Demo Suppliers: ${suppliers.length}`);
suppliers.forEach(s => console.log(` ${s.inn}${s.companyName} [${s.productTags.join(', ')}]`));
const products = await prisma.product.findMany({
where: { code: { startsWith: ЕМО_PROD_' } },
select: { id: true, code: true, name: true, status: true },
});
console.log(` Demo Products: ${products.length}`);
for (const p of products) {
const smetaLineCount = await prisma.smetaLine.count({
where: { smeta: { productId: p.id } },
});
const pricingCount = await prisma.productMaterialPricing.count({
where: { productId: p.id, status: 'ACTIVE' },
});
console.log(` ${p.code}${p.name} (${p.status}) | smeta_lines=${smetaLineCount} | pricings=${pricingCount}`);
}
}
async function main() {
console.log('=== SEED BETA DEMO DATA ===');
console.log(`Database: ${process.env.DATABASE_URL?.replace(/:[^:@]+@/, ':***@') ?? 'not set'}`);
const adminUserId = await getAdminUserId();
console.log(`Admin user: ${adminUserId}`);
await seedClients();
const supplierIds = await seedSuppliers();
await seedProducts(adminUserId, supplierIds);
await verifyReferentials();
await printSummary();
console.log('\n=== DONE ===');
}
main()
.catch(e => { console.error(e); process.exit(1); })
.finally(() => prisma.$disconnect());