/** * Локальная разработка: демо-клиенты для новой админки (дашборд, список клиентов, подписки, деньги). * 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 = (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 } } }); // демо-чаты (dev:chats-demo) ссылаются на клиентов await db.chatThread.deleteMany({ where: { clientId: { in: oldClientIds } } }); await db.conciergeMessage.deleteMany({ where: { clientId: { 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 | null, '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();