Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-25 22:55:29 +04:00
1 parent 45d7512d2b
commit 3489e0b816
319 files changed
+45483 -121

No files matched your search

+205
View File
@@ -0,0 +1,205 @@
/**
* Локальная разработка: демо-клиенты для новой админки (дашборд, список клиентов, подписки, деньги).
* 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();