- 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>
340 lines
13 KiB
TypeScript
340 lines
13 KiB
TypeScript
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());
|