Files
apollo-backend/scripts/seed-demo.js
2026-05-16 17:16:31 +07:00

537 lines
16 KiB
JavaScript
Raw Permalink 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 '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();
});