// Дополнительные клиенты для демо-компаний (после 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}`); }