Lead Radar: исходный код

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuriandClaude Opus 5.5 committed 2026-10-25 23:11:04 +04:00
commit 9749da5ae7
201 files changed
+97685

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
// Дополнительные клиенты для демо-компаний (после seed-demo.mjs): по десять на компанию,
// на разных этапах, с подобранными объектами, комментариями и напоминаниями.
// Повторный запуск безопасен: клиенты с уже существующим именем пропускаются.
//
// BASE=https://… DEMO_PASSWORD=… node scripts/seed-demo-clients.mjs
const BASE = (process.env.BASE ?? 'http://localhost:3100').replace(/\/$/, '');
const { DEMO_PASSWORD } = process.env;
if (!DEMO_PASSWORD) throw new Error('Нужен DEMO_PASSWORD');
const call = (token) => async (method, path, body) => {
const res = await fetch(`${BASE}/api${path}`, {
method,
headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body !== undefined ? { 'content-type': 'application/json' } : {}) },
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${path} → ${res.status} ${JSON.stringify(data)}`);
return data;
};
const login = async (email) => call((await call()('POST', '/auth/login', { email, password: DEMO_PASSWORD })).token);
const day = (offset) => new Date(Date.now() + offset * 86_400_000);
const apt = ['apartment'];
const amd = (priceTo, extra = {}) => ({ propertyTypes: apt, region: 'Ереван', priceTo, priceCurrency: 'AMD', ...extra });
const usd = (priceTo, extra = {}) => ({ propertyTypes: apt, region: 'Ереван', priceTo, priceCurrency: 'USD', ...extra });
// stage: 'new' — только заведён; число — этап воронки по порядку; 'rejected' — отказ.
const DEMOS = [
{
key: 'rent', emails: ['demo-rent@yandp.demo', 'demo-rent-1@yandp.demo', 'demo-rent-2@yandp.demo'], phoneBase: 1100,
clients: [
{ name: 'Ольга Белова', kind: 'tenant', source: 'Анкета в Telegram', request: amd(450000, { rooms: [1, 2] }), household: { who: 'alone', pets: { count: 1, age: 'кошка, 2 года' } }, stage: 'new' },
{ name: 'Самвел и Нуне', kind: 'tenant', source: 'Рекомендация', request: amd(700000, { rooms: [2, 3], districts: ['Арабкир', 'Кентрон'] }), household: { who: 'couple' }, stage: 0, comment: 'Переезжают в начале месяца, нужна мебель и техника.' },
{ name: 'Дмитрий Орлов', kind: 'tenant', source: 'Instagram', request: usd(1200, { districts: ['Кентрон'], rooms: [2] }), household: { who: 'alone' }, stage: 1, comment: 'Работает удалённо, важен быстрый интернет и тихий двор.' },
{ name: 'Семья Хачатрян', kind: 'tenant', source: 'List.am', request: { propertyTypes: ['apartment', 'house'], region: 'Ереван', priceTo: 1000000, priceCurrency: 'AMD', rooms: [3, 4] }, household: { who: 'couple', children: { count: 3, age: '2, 5 и 8 лет' } }, stage: 2, comment: 'Нужна школа рядом. Показали два варианта, второй понравился.' },
{ name: 'Кристина Ли', kind: 'tenant', source: 'Сайт', request: usd(900, { rooms: [1, 2] }), household: { who: 'friend' }, stage: 0, comment: 'Снимают вдвоём с подругой, на полгода с продлением.' },
{ name: 'Гарик Мелконян', kind: 'tenant', source: 'Рекомендация', request: amd(300000, { rooms: [1] }), household: { who: 'alone' }, stage: 'rejected' },
{ name: 'Наталья и Игорь', kind: 'tenant', source: 'Анкета в Telegram', request: usd(1800, { rooms: [3], districts: ['Кентрон', 'Арабкир'] }), household: { who: 'couple', children: { count: 1, age: '1 год' }, pets: { count: 1, age: 'собака, 6 лет' } }, stage: 1, comment: 'С собакой — заранее уточнять у собственника.' },
{ name: 'Ашот Варданян', kind: 'tenant', source: 'Звонок', request: amd(550000, { rooms: [2, 3] }), household: { who: 'couple', children: { count: 2, age: '10 и 13 лет' } }, stage: 'new' },
{ name: 'Эмили Картер', kind: 'tenant', source: 'Партнёр', request: usd(2500, { rooms: [2, 3], districts: ['Кентрон'] }), household: { who: 'alone' }, stage: 2, comment: 'Релокация от компании, договор на год, оплата по счёту.' },
{ name: 'Студенты (трое)', kind: 'tenant', source: 'Instagram', request: amd(400000, { rooms: [2, 3] }), household: { who: 'friend' }, stage: 'rejected' },
],
},
{
key: 'sale', emails: ['demo-sale@yandp.demo', 'demo-sale-1@yandp.demo', 'demo-sale-2@yandp.demo'], phoneBase: 1150,
clients: [
{ name: 'Арам Погосян', kind: 'buyer', source: 'Рекомендация', request: usd(150000, { rooms: [2, 3] }), stage: 'new' },
{ name: 'Ирина Волкова', kind: 'buyer', source: 'Instagram', request: usd(220000, { rooms: [3], districts: ['Арабкир'] }), stage: 0, comment: 'Покупает за наличные, готова выйти на сделку быстро.' },
{ name: 'Семья Давтян', kind: 'buyer', source: 'List.am', request: { propertyTypes: ['house'], region: 'Ереван', priceTo: 350000, priceCurrency: 'USD' }, stage: 1, comment: 'Нужен дом с участком, рассматривают ипотеку.' },
{ name: 'Левон Тер-Петросян', kind: 'buyer', source: 'Сайт', request: usd(95000, { rooms: [1, 2] }), stage: 2, comment: 'Первая квартира, одобрена ипотека до $95 000.' },
{ name: 'Алексей и Мария', kind: 'buyer', source: 'Партнёр', request: usd(300000, { rooms: [3, 4], districts: ['Кентрон'] }), stage: 1, comment: 'Переезд из Москвы, смотрят новостройки в центре.' },
{ name: 'Сюзанна Айвазян', kind: 'buyer', source: 'Звонок', request: usd(130000, { rooms: [2] }), stage: 'rejected' },
{ name: 'Инвестор (2 квартиры)', kind: 'buyer', source: 'Рекомендация', request: usd(110000, { rooms: [1, 2], districts: ['Кентрон', 'Арабкир'] }), stage: 2, comment: 'Берёт под сдачу, важна доходность. Показали три объекта.' },
{ name: 'Овик Назарян', kind: 'buyer', source: 'List.am', request: usd(170000, { rooms: [3] }), stage: 0, comment: 'Сначала должен продать свою квартиру.' },
{ name: 'Джон Мюллер', kind: 'buyer', source: 'Сайт', request: usd(450000, { rooms: [3, 4], districts: ['Кентрон'] }), stage: 'new' },
{ name: 'Гаяне Симонян', kind: 'buyer', source: 'Instagram', request: usd(80000, { rooms: [1] }), stage: 'rejected' },
],
},
];
const NOTES = ['Перезвонить по вариантам', 'Согласовать время показа', 'Отправить новую подборку', 'Уточнить бюджет'];
for (const demo of DEMOS) {
const people = [];
for (const email of demo.emails) people.push(await login(email));
const [boss, a1, a2] = people;
// Агентам — по четыре клиента, руководителю — два.
const owners = [a1, a2, a1, a2, boss, a1, a2, a1, a2, boss];
const pipe = await boss('GET', '/pipelines/client');
const middle = pipe.statuses.filter((s) => !s.code).map((s) => s.id);
const rejected = pipe.statuses.find((s) => s.code === 'rejected').id;
const have = new Set((await boss('GET', '/clients?tab=all')).map((c) => c.name));
let added = 0;
for (const [i, { stage, comment, ...c }] of demo.clients.entries()) {
if (have.has(c.name)) continue;
const api = owners[i];
const { id } = await api('POST', '/clients', { ...c, phone: `+3749900${demo.phoneBase + i}` });
added++;
if (stage === 'new') continue;
const upTo = stage === 'rejected' ? 0 : Math.min(stage, middle.length - 1);
// Чем дальше этап, тем больше объектов уже подобрано; первый — показан.
const fit = (await api('GET', `/clients/${id}/suggestions`)).slice(0, stage === 'rejected' ? 1 : upTo + 1);
for (const [k, lead] of fit.entries()) {
await api('POST', `/clients/${id}/leads`, { leadId: lead.id, state: k === 0 && upTo > 0 ? 'shown' : 'offered' });
}
for (let k = 0; k <= upTo; k++) await api('POST', `/clients/${id}/status`, { statusId: middle[k] });
if (comment) await api('POST', `/clients/${id}/comments`, { text: comment });
if (stage === 'rejected') {
await api('POST', `/clients/${id}/status`, { statusId: rejected, rejectReasonId: pipe.reasons[i % pipe.reasons.length].id });
} else {
// Одно напоминание из пяти — просроченное, как бывает в жизни.
await api('POST', `/clients/${id}/next-action`, { at: day(i % 5 === 3 ? -1 : 1 + (i % 4)).toISOString(), note: NOTES[i % NOTES.length] });
}
}
console.log(`${demo.key}: добавлено клиентов — ${added}`);
}