import 'dotenv/config'; import { prisma } from '../src/prisma-client.js'; const MANAGER_EMAIL = 'manager@fregat.local'; const DEMO_EMAIL_DOMAIN = 'demo.fregat.local'; const DEMO_ORDER_PREFIX = 'DBG-'; const DEMO_CLIENT_COUNT = Number.parseInt(process.env.DEMO_CLIENTS ?? '20', 10); const DEMO_ORDER_COUNT = Number.parseInt(process.env.DEMO_ORDERS ?? '40', 10); const FIRST_NAMES = [ 'Алексей', 'Мария', 'Ирина', 'Дмитрий', 'Светлана', 'Павел', 'Ольга', 'Иван', 'Наталья', 'Егор', 'Виктория', 'Максим', 'Елена', 'Роман', 'Анна', 'Кирилл', 'Юлия', 'Андрей', 'Татьяна', 'Сергей', ]; const LAST_NAMES = [ 'Иванов', 'Петрова', 'Смирнова', 'Козлов', 'Васильева', 'Федоров', 'Морозова', 'Захаров', 'Орлова', 'Новиков', 'Романова', 'Соколов', 'Беляева', 'Громов', 'Крылова', 'Титов', 'Борисова', 'Попов', 'Лебедева', 'Макаров', ]; const CITIES = [ 'Москва', 'Санкт-Петербург', 'Казань', 'Екатеринбург', 'Новосибирск', 'Нижний Новгород', 'Самара', 'Краснодар', 'Ростов-на-Дону', 'Воронеж', ]; const STREETS = [ 'Ленинградский проспект', 'Кубинская улица', 'улица Родины', 'Промышленная улица', 'улица Победы', 'Складской проезд', 'Транспортная улица', 'улица Энергетиков', 'Рабочая улица', 'Индустриальный проспект', ]; const COMPANY_PREFIXES = [ 'ТД', 'ПК', 'Логистик', 'Сервис', 'Группа', 'Снаб', 'Пром', 'Регион', 'Партнер', 'Трейд', ]; const COMPANY_SUFFIXES = [ 'Пласт', 'Пак', 'Транс', 'Маркет', 'Снабжение', 'Лайн', 'Система', 'Поставка', 'Ритейл', 'Ресурс', ]; const ORDER_STATUS_CYCLE = [ 'NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED', ]; function formatIndex(index) { return String(index).padStart(2, '0'); } function buildClientEmail(index) { return `client${formatIndex(index)}@${DEMO_EMAIL_DOMAIN}`; } function buildInn(index) { return `7702${String(index).padStart(6, '0')}`; } function buildOgrn(index) { return `102770${String(index).padStart(7, '0')}`; } function buildBik(index) { return `0445${String(10000 + index).slice(-5)}`; } function buildAccount(prefix, index) { return `${prefix}${String(100000000000000000 + index).slice(-18)}`; } function atMiddayDaysAgo(daysAgo) { const date = new Date(); date.setHours(12, 0, 0, 0); date.setDate(date.getDate() - daysAgo); return date; } function addHours(date, hours) { return new Date(date.getTime() + (hours * 60 * 60 * 1000)); } function toMoney(value) { return value.toFixed(2); } function createOrderTimeline(status, createdAt) { const steps = [ { status: 'NEW', note: '[demo] Заказ создан клиентом.' }, { status: 'MANAGER_PROCESSING', note: '[demo] Менеджер взял заказ в работу.' }, { status: 'WAITING_DOUBLE_CONFIRM', note: '[demo] Согласование условий и цены.' }, { status: 'CONFIRMED', note: '[demo] Стороны подтвердили заказ.' }, { status: 'IN_PROGRESS', note: '[demo] Заказ передан в исполнение.' }, { status: 'COMPLETED', note: '[demo] Заказ доставлен клиенту.' }, ]; const targetIndex = steps.findIndex((step) => step.status === status); if (targetIndex === -1) { return [steps[0]]; } return steps.slice(0, targetIndex + 1).map((step, index) => ({ ...step, createdAt: addHours(createdAt, index * 6), })); } function orderKindForIndex(index) { return index % 5 === 0 ? 'CALCULATION' : 'READY'; } function orderStatusForIndex(index) { return ORDER_STATUS_CYCLE[index % ORDER_STATUS_CYCLE.length]; } function companyNameForIndex(index) { const prefix = COMPANY_PREFIXES[index % COMPANY_PREFIXES.length]; const suffix = COMPANY_SUFFIXES[index % COMPANY_SUFFIXES.length]; return `${prefix} ${suffix} ${formatIndex(index)}`; } function fullNameForIndex(index) { return `${FIRST_NAMES[(index - 1) % FIRST_NAMES.length]} ${LAST_NAMES[(index - 1) % LAST_NAMES.length]}`; } async function upsertClient(index) { const companyName = companyNameForIndex(index); const company = await prisma.company.upsert({ where: { inn: buildInn(index) }, update: { name: companyName }, create: { name: companyName, inn: buildInn(index), }, }); const user = await prisma.user.upsert({ where: { email: buildClientEmail(index) }, update: { fullName: fullNameForIndex(index), role: 'CLIENT', bonusProgramEnabled: index % 2 === 1, companyId: company.id, }, create: { email: buildClientEmail(index), fullName: fullNameForIndex(index), role: 'CLIENT', bonusProgramEnabled: index % 2 === 1, companyId: company.id, }, }); await prisma.counterpartyProfile.upsert({ where: { userId: user.id }, update: { companyName, companyFullName: `${companyName}, общество с ограниченной ответственностью`, inn: buildInn(index), kpp: `7702${String(2000 + index).slice(-4)}`, ogrn: buildOgrn(index), legalAddress: `${CITIES[(index - 1) % CITIES.length]}, ${STREETS[(index - 1) % STREETS.length]}, ${10 + index}`, bankName: 'АО Тест Банк', bik: buildBik(index), correspondentAccount: buildAccount('30101', index), checkingAccount: buildAccount('40702', index), signerFullName: fullNameForIndex(index), signerPosition: 'Генеральный директор', signerBasis: 'Устав', }, create: { userId: user.id, companyName, companyFullName: `${companyName}, общество с ограниченной ответственностью`, inn: buildInn(index), kpp: `7702${String(2000 + index).slice(-4)}`, ogrn: buildOgrn(index), legalAddress: `${CITIES[(index - 1) % CITIES.length]}, ${STREETS[(index - 1) % STREETS.length]}, ${10 + index}`, bankName: 'АО Тест Банк', bik: buildBik(index), correspondentAccount: buildAccount('30101', index), checkingAccount: buildAccount('40702', index), signerFullName: fullNameForIndex(index), signerPosition: 'Генеральный директор', signerBasis: 'Устав', }, }); const addressLabel = 'Основной адрес'; const addressValue = `${CITIES[(index - 1) % CITIES.length]}, ${STREETS[(index - 1) % STREETS.length]}, ${20 + index}`; const existingAddress = await prisma.deliveryAddress.findFirst({ where: { userId: user.id, label: addressLabel, }, }); const address = existingAddress ? await prisma.deliveryAddress.update({ where: { id: existingAddress.id }, data: { address: addressValue, unrestrictedValue: addressValue, }, }) : await prisma.deliveryAddress.create({ data: { userId: user.id, label: addressLabel, address: addressValue, unrestrictedValue: addressValue, }, }); await prisma.user.update({ where: { id: user.id }, data: { defaultDeliveryAddressId: address.id, }, }); return { user, address }; } async function rebuildMessengerConnections(clients) { await prisma.messengerConnection.deleteMany({ where: { userId: { in: clients.map((entry) => entry.user.id), }, }, }); for (const [index, client] of clients.entries()) { const demoIndex = index + 1; if (demoIndex % 2 === 1) { await prisma.messengerConnection.create({ data: { userId: client.user.id, type: 'TELEGRAM', channelId: `70000${demoIndex}`, displayName: client.user.fullName, username: `fregat_demo_${formatIndex(demoIndex)}`, }, }); } if (demoIndex % 3 === 0) { await prisma.messengerConnection.create({ data: { userId: client.user.id, type: 'MAX', channelId: `90000${demoIndex}`, displayName: client.user.fullName, username: `max_demo_${formatIndex(demoIndex)}`, }, }); } } } async function cleanupDemoData(demoUserIds) { const demoOrders = await prisma.order.findMany({ where: { OR: [ { code: { startsWith: DEMO_ORDER_PREFIX } }, { customerId: { in: demoUserIds } }, ], }, select: { id: true }, }); const demoOrderIds = demoOrders.map((order) => order.id); if (demoOrderIds.length) { await prisma.orderStatusEvent.deleteMany({ where: { orderId: { in: demoOrderIds } }, }); await prisma.orderItem.deleteMany({ where: { orderId: { in: demoOrderIds } }, }); await prisma.bonusTransaction.deleteMany({ where: { orderId: { in: demoOrderIds } }, }); await prisma.order.deleteMany({ where: { id: { in: demoOrderIds } }, }); } await prisma.rewardWithdrawalRequest.deleteMany({ where: { requesterId: { in: demoUserIds } }, }); await prisma.bonusTransaction.deleteMany({ where: { userId: { in: demoUserIds }, reason: { startsWith: '[demo]' }, }, }); await prisma.referralLink.deleteMany({ where: { OR: [ { referrerId: { in: demoUserIds } }, { refereeId: { in: demoUserIds } }, ], }, }); } async function createReferralLinks(managerId, clients) { const links = []; for (let index = 0; index < Math.min(10, Math.floor(clients.length / 2)); index += 1) { const referrer = clients[index].user; const referee = clients[index + 10]?.user; if (!referee) { continue; } const link = await prisma.referralLink.create({ data: { referrerId: referrer.id, refereeId: referee.id, createdById: managerId, bonusPercent: toMoney(5 + (index % 4) * 2.5), }, }); links.push(link); } return links; } async function createOrders(managerId, clients, products, referralLinks) { const referralByRefereeId = new Map(referralLinks.map((link) => [link.refereeId, link])); let completedOrders = 0; for (let index = 1; index <= DEMO_ORDER_COUNT; index += 1) { const client = clients[(index - 1) % clients.length]; const createdAt = atMiddayDaysAgo(DEMO_ORDER_COUNT - index + 1); const status = orderStatusForIndex(index - 1); const kind = orderKindForIndex(index); const itemCount = 1 + (index % 3); const orderProducts = Array.from({ length: itemCount }, (_, itemIndex) => ( products[(index + itemIndex * 3) % products.length] )); const preparedItems = orderProducts.map((product, itemIndex) => { const quantity = 5 + ((index + itemIndex * 2) % 18); const unitPrice = 72 + ((index * 11 + itemIndex * 7) % 65); return { product, quantity, unitPrice, lineTotal: quantity * unitPrice, }; }); const totalPrice = preparedItems.reduce((sum, item) => sum + item.lineTotal, 0); const deliveryFee = 1200 + (index % 5) * 350; const order = await prisma.order.create({ data: { code: `${DEMO_ORDER_PREFIX}${String(3000 + index)}`, kind, customerId: client.user.id, managerId, deliveryAddressId: client.address.id, deliveryAddress: client.address.address, deliveryTerms: index % 4 === 0 ? 'Доставка до адреса клиента' : 'Самовывоз со склада после подтверждения', deliveryFee: toMoney(deliveryFee), totalPrice: toMoney(totalPrice + deliveryFee), status, clientApproved: ['CONFIRMED', 'IN_PROGRESS', 'COMPLETED'].includes(status) ? true : null, managerApproved: ['WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED'].includes(status) ? true : null, calculationPayload: kind === 'CALCULATION' ? { note: 'Демо-расчет для интерфейсов менеджера.', requestedAt: createdAt.toISOString(), } : null, createdAt, updatedAt: addHours(createdAt, 12), }, }); for (const item of preparedItems) { await prisma.orderItem.create({ data: { orderId: order.id, productId: item.product.id, productName: item.product.name, quantity: item.quantity.toFixed(3), unitPrice: toMoney(item.unitPrice), createdAt, }, }); } const timeline = createOrderTimeline(status, createdAt); for (const event of timeline) { await prisma.orderStatusEvent.create({ data: { orderId: order.id, status: event.status, actorUserId: managerId, note: event.note, createdAt: event.createdAt, }, }); } if (status === 'COMPLETED') { completedOrders += 1; const referralLink = referralByRefereeId.get(client.user.id); if (referralLink) { const bonusAmount = Number((Number(totalPrice + deliveryFee) * Number(referralLink.bonusPercent) / 100).toFixed(2)); await prisma.bonusTransaction.create({ data: { userId: referralLink.referrerId, amount: toMoney(bonusAmount), reason: `[demo] Бонус за заказ ${order.code}`, orderId: order.id, referralLinkId: referralLink.id, createdAt: addHours(createdAt, 18), }, }); } } } return completedOrders; } async function createWithdrawalDrafts(clients) { const candidates = clients.slice(0, 3); for (const [index, client] of candidates.entries()) { await prisma.rewardWithdrawalRequest.create({ data: { requesterId: client.user.id, amount: toMoney(1500 + index * 750), status: 'PENDING', reviewComment: '[demo] Тестовая заявка на вывод.', }, }); } } async function main() { if (!Number.isFinite(DEMO_CLIENT_COUNT) || DEMO_CLIENT_COUNT < 2) { throw new Error('DEMO_CLIENTS must be at least 2.'); } if (!Number.isFinite(DEMO_ORDER_COUNT) || DEMO_ORDER_COUNT < 1) { throw new Error('DEMO_ORDERS must be at least 1.'); } const manager = await prisma.user.findUnique({ where: { email: MANAGER_EMAIL }, }); if (!manager) { throw new Error(`Manager ${MANAGER_EMAIL} not found. Run npm run seed first.`); } const products = await prisma.product.findMany({ where: { isActive: true }, orderBy: { sku: 'asc' }, take: 24, }); if (products.length < 6) { throw new Error('Not enough active products. Run npm run seed first.'); } const clients = []; for (let index = 1; index <= DEMO_CLIENT_COUNT; index += 1) { clients.push(await upsertClient(index)); } const demoUserIds = clients.map((entry) => entry.user.id); await cleanupDemoData(demoUserIds); await rebuildMessengerConnections(clients); const referralLinks = await createReferralLinks(manager.id, clients); const completedOrders = await createOrders(manager.id, clients, products, referralLinks); await createWithdrawalDrafts(clients); const [usersCount, ordersCount] = await Promise.all([ prisma.user.count({ where: { email: { endsWith: `@${DEMO_EMAIL_DOMAIN}` }, }, }), prisma.order.count({ where: { code: { startsWith: DEMO_ORDER_PREFIX }, }, }), ]); console.log(`Demo seed complete: ${usersCount} demo clients, ${ordersCount} demo orders, ${referralLinks.length} referral links, ${completedOrders} completed orders.`); } await main() .finally(async () => { await prisma.$disconnect(); });