Files
BarevClub/apps/api/scripts/dev-backoffice-demo.ts
T
2026-10-25 22:55:29 +04:00

206 lines
9.9 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.
/**
* Локальная разработка: демо-клиенты для новой админки (дашборд, список клиентов, подписки, деньги).
* pnpm --filter @barev/api dev:bo-demo
* Клиенты помечены тегом «демо» и telegramId 100001000+. Повторный запуск пересоздаёт их.
* Цены условные — реальных цен подписки пока нет.
*/
import { getDb } from '@barev/db';
import { env } from '../src/env.ts';
if (env.isProd) throw new Error('Только для разработки');
const db = getDb();
const DAY = 24 * 3600_000;
const now = Date.now();
const TG_FROM = 100001000n;
// детерминированный «случай» — одинаковые данные при каждом запуске
let seed = 42;
const rnd = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
const pick = <T,>(a: readonly T[]) => a[Math.floor(rnd() * a.length)]!;
const between = (a: number, b: number) => Math.round(a + rnd() * (b - a));
// ── чистим прошлое демо ──
const old = await db.user.findMany({ where: { telegramId: { gte: TG_FROM, lt: TG_FROM + 1000n } }, include: { client: true } });
const oldClientIds = old.map((u) => u.client?.id).filter((x): x is string => !!x);
await db.rating.deleteMany({ where: { clientId: { in: oldClientIds } } });
await db.order.deleteMany({ where: { clientId: { in: oldClientIds } } });
await db.subscription.deleteMany({ where: { clientId: { in: oldClientIds } } });
await db.cardUsage.deleteMany({ where: { clientId: { in: oldClientIds } } });
await db.auditLog.deleteMany({ where: { entityId: { in: oldClientIds } } });
await db.client.deleteMany({ where: { id: { in: oldClientIds } } });
await db.user.deleteMany({ where: { id: { in: old.map((u) => u.id) } } });
await db.lead.deleteMany({ where: { contact: { startsWith: '+374 99 00' } } });
const [services, agencies, providers, partners, owner] = await Promise.all([
db.service.findMany({ where: { active: true } }),
db.agency.findMany({ where: { active: true } }),
db.provider.findMany({ where: { active: true } }),
db.partner.findMany({ where: { active: true } }),
db.adminUser.findFirst({ where: { roles: { has: 'SUPERADMIN' } } }),
]);
const PEOPLE: [string, string | null, string, 'ru' | 'en' | 'hy' | 'zh' | 'es'][] = [
['Мария', 'Иванова', 'masha_iv', 'ru'],
['Дмитрий', 'Козлов', 'dkozlov', 'ru'],
['Sarah', 'Miller', 'sarah_m', 'en'],
['Арсен', 'Григорян', 'arsen_g', 'hy'],
['Екатерина', 'Смирнова', 'katya_sm', 'ru'],
['Alex', 'Novak', 'alexnovak', 'en'],
['Павел', 'Орлов', 'p_orlov', 'ru'],
['Наталья', 'Белова', 'nbelova', 'ru'],
['Tom', 'Becker', 'tbecker', 'en'],
['Ольга', 'Лебедева', 'olga_leb', 'ru'],
['Игорь', 'Соколов', 'igor_s', 'ru'],
['Lucía', 'Fernández', 'lucia_f', 'es'],
['Анна', 'Морозова', 'annamoroz', 'ru'],
['王', '伟', 'wangwei', 'zh'],
['Сергей', 'Волков', 'svolkov', 'ru'],
['Елена', 'Кузнецова', 'elenakuz', 'ru'],
['Карен', 'Арутюнян', 'karen_a', 'hy'],
['Max', 'Fischer', 'maxfi', 'en'],
['Юлия', 'Попова', 'yulia_pop', 'ru'],
['Андрей', 'Зайцев', 'azaitsev', 'ru'],
['Ирина', 'Павлова', 'irina_p', 'ru'],
['Никита', 'Семёнов', 'nik_sem', 'ru'],
['Emma', 'Wilson', 'emmaw', 'en'],
['Светлана', 'Голубева', 'sveta_g', 'ru'],
['Михаил', 'Виноградов', 'mvinogradov', 'ru'],
['Лилит', 'Карапетян', 'lilit_k', 'hy'],
['Артём', 'Богданов', 'artem_b', 'ru'],
['Ксения', 'Фёдорова', 'ksu_f', 'ru'],
['Daniel', 'Costa', 'dcosta', 'es'],
['Виктор', 'Михайлов', 'v_mikh', 'ru'],
['Алина', 'Комарова', 'alina_k', 'ru'],
['Роман', 'Новиков', 'r_novikov', 'ru'],
['Полина', 'Егорова', null, 'ru'],
['Григорий', 'Ткаченко', 'grisha_t', 'ru'],
['Hannah', 'Schmidt', 'hannah_s', 'en'],
['Вера', 'Андреева', 'vera_a', 'ru'],
['Олег', 'Николаев', 'olegnik', 'ru'],
['Дарья', 'Ковалёва', 'dasha_k', 'ru'],
];
const TAGS = ['VIP', 'Кентрон', 'с детьми', 'IT', 'просит звонок', 'жалоба'];
const UTMS = ['instagram', 'telegram_ads', 'google'];
const PRICE = { BAREV_PLUS: 4900, ELITE: 19900 } as const;
let created = 0;
for (const [i, [firstName, lastName, username, locale]] of PEOPLE.entries()) {
const ageDays = between(0, 58);
const createdAt = new Date(now - ageDays * DAY - between(0, 20) * 3600_000);
const user = await db.user.create({
data: { telegramId: TG_FROM + BigInt(i), firstName, lastName, username: username ?? undefined, locale, phone: rnd() < 0.5 ? `+374 9${between(1, 9)} ${between(100, 999)} ${between(100, 999)}` : undefined, createdAt },
});
const srcRoll = rnd();
const agency = srcRoll < 0.35 && agencies.length ? pick(agencies) : null;
const utm = !agency && srcRoll < 0.6 ? { utm_source: pick(UTMS) } : undefined;
const tags = rnd() < 0.35 ? [pick(TAGS), ...(rnd() < 0.3 ? [pick(TAGS)] : [])] : [];
const client = await db.client.create({
data: {
userId: user.id,
cardCode: `D${String(1000 + i)}${Math.floor(rnd() * 90 + 10)}`,
source: agency ? (rnd() < 0.7 ? 'deeplink' : 'manual_code') : 'organic',
agencyId: agency?.id,
agencyAttachedAt: agency ? createdAt : undefined,
utm,
consentAt: createdAt,
consentVersion: '1',
segment: rnd() < 0.4 ? 'MOVING' : 'LIVING',
tags: [...new Set(['демо', ...tags])],
createdAt,
},
});
created++;
// подписки
const subRoll = rnd();
if (agency && rnd() < 0.8) {
const s = new Date(createdAt.getTime() + 3600_000);
await db.subscription.create({ data: { clientId: client.id, plan: 'BAREV_PLUS', source: 'AGENCY_GIFT', agencyId: agency.id, startsAt: s, endsAt: new Date(s.getTime() + 30 * DAY), createdAt: s } });
// часть подарков превращается в покупку
if (rnd() < 0.45 && ageDays > 20) {
const ps = new Date(s.getTime() + 30 * DAY);
await db.subscription.create({
data: { clientId: client.id, plan: 'BAREV_PLUS', source: 'PAID', amount: PRICE.BAREV_PLUS * 3, payMethod: pick(['cash', 'transfer', 'card']), grantedByAdminId: owner?.id, startsAt: ps, endsAt: new Date(ps.getTime() + 90 * DAY), createdAt: new Date(ps.getTime() - 2 * DAY) },
});
}
} else if (subRoll < 0.45) {
const plan = rnd() < 0.25 ? 'ELITE' : 'BAREV_PLUS';
const months = pick([1, 1, 3, 6, 12]);
const bought = new Date(createdAt.getTime() + between(0, Math.max(1, ageDays - 1)) * DAY);
// часть подписок истекает на этой неделе
const endsAt = rnd() < 0.25 ? new Date(now + between(1, 6) * DAY) : new Date(bought.getTime() + months * 30 * DAY);
await db.subscription.create({
data: { clientId: client.id, plan, source: 'PAID', amount: PRICE[plan] * months, payMethod: pick(['cash', 'transfer', 'card']), grantedByAdminId: owner?.id, startsAt: bought, endsAt, createdAt: bought },
});
} else if (subRoll < 0.6) {
const s = new Date(createdAt.getTime() + DAY);
await db.subscription.create({
data: { clientId: client.id, plan: 'BAREV_PLUS', source: 'MANUAL', grantedByAdminId: owner?.id, note: 'Подарок за отзыв', startsAt: s, endsAt: new Date(s.getTime() + 30 * DAY), createdAt: s },
});
}
// оплаченные заказы
const nOrders = rnd() < 0.55 ? between(1, 4) : 0;
for (let k = 0; k < nOrders && services.length; k++) {
const svc = pick(services);
const at = new Date(now - between(0, Math.max(0, ageDays)) * DAY - between(1, 10) * 3600_000);
const price = pick([12000, 15000, 18000, 25000, 30000, 45000, 60000]);
const provider = providers.length ? pick(providers) : null;
const order = await db.order.create({
data: {
clientId: client.id,
serviceId: svc.id,
params: {},
scheduledAt: at,
durationMinutes: 120,
locale,
basePrice: price,
totalPrice: price,
status: 'DONE',
providerId: provider?.id,
paymentStatus: 'PAID',
paidAt: new Date(at.getTime() + 3 * 3600_000),
completedAt: at,
createdAt: new Date(at.getTime() - DAY),
},
});
if (provider && rnd() < 0.6) {
await db.rating.create({ data: { orderId: order.id, clientId: client.id, providerId: provider.id, score: pick([5, 5, 5, 4, 4, 3, 2]), createdAt: new Date(at.getTime() + 5 * 3600_000) } });
}
}
// сканы клубной карты у заведений
if (partners.length && rnd() < 0.5) {
for (let k = between(1, 5); k > 0; k--) {
await db.cardUsage.create({ data: { clientId: client.id, partnerId: pick(partners).id, level: 'BAREV_PLUS', createdAt: new Date(now - between(0, 29) * DAY) } });
}
}
}
// заявки с сайта для воронки
for (let k = 0; k < 14; k++) {
await db.lead.create({
data: {
key: `demo-${k}`,
name: pick(PEOPLE)[0],
contact: `+374 99 00 ${String(k).padStart(2, '0')} 00`,
contactType: 'PHONE',
locale: 'ru',
consentAt: new Date(),
consentVersion: '1',
status: k < 3 ? 'NEW' : 'DONE',
createdAt: new Date(now - between(0, 29) * DAY),
},
});
}
// пересчёт рейтингов исполнителей
for (const p of providers) {
const agg = await db.rating.aggregate({ where: { providerId: p.id }, _sum: { score: true }, _count: true });
await db.provider.update({ where: { id: p.id }, data: { ratingSum: agg._sum.score ?? 0, ratingCount: agg._count } });
}
console.log(`Демо для новой админки: ${created} клиентов. Открывайте http://localhost:5176`);
await db.$disconnect();