import { PrismaClient } from "../server/generated/prisma/client.js"; import { PrismaPg } from "@prisma/adapter-pg"; import fs from "node:fs"; import path from "node:path"; import { randomBytes, scryptSync } from "node:crypto"; function loadEnvFromDotEnv() { const p = path.resolve(process.cwd(), ".env"); if (!fs.existsSync(p)) return; const raw = fs.readFileSync(p, "utf8"); for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const idx = trimmed.indexOf("="); if (idx === -1) continue; const key = trimmed.slice(0, idx).trim(); let val = trimmed.slice(idx + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (!key) continue; if (key === "DATABASE_URL") { if (!process.env[key]) process.env[key] = val; continue; } if (!process.env[key]) process.env[key] = val; } } loadEnvFromDotEnv(); const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); const LOGIN_PHONE = "+15550000001"; const LOGIN_PASSWORD = "ConnectFlow#2026"; const LOGIN_NAME = "Владелец Connect"; const REF_DATE_ISO = "2026-02-20T12:00:00.000Z"; const SCRYPT_KEY_LENGTH = 64; function hashPassword(password) { const salt = randomBytes(16).toString("base64url"); const digest = scryptSync(password, salt, SCRYPT_KEY_LENGTH).toString("base64url"); return `scrypt$${salt}$${digest}`; } function atOffset(days, hour, minute) { const d = new Date(REF_DATE_ISO); d.setDate(d.getDate() + days); d.setHours(hour, minute, 0, 0); return d; } function plusMinutes(date, minutes) { const d = new Date(date); d.setMinutes(d.getMinutes() + minutes); return d; } function buildOdooAiContacts(teamId) { const prospects = [ { name: "Оливия Рид", email: "olivia.reed@retailnova.com", phone: "+1 555 120 0101" }, { name: "Даниэль Ким", email: "daniel.kim@forgepeak.com", phone: "+1 555 120 0102" }, { name: "Марта Алонсо", email: "marta.alonso@iberiafoods.es", phone: "+34 91 555 0103" }, { name: "Юсеф Хаддад", email: "youssef.haddad@gulftrade.ae", phone: "+971 4 555 0104" }, { name: "Эмма Коллинз", email: "emma.collins@northbridge.co.uk", phone: "+44 20 5550 0105" }, { name: "Ноа Фишер", email: "noah.fischer@bergmann-auto.de", phone: "+49 89 5550 0106" }, { name: "Ава Чой", email: "ava.choi@pacificmedtech.sg", phone: "+65 6555 0107" }, { name: "Лиам Дюбуа", email: "liam.dubois@hexacommerce.fr", phone: "+33 1 55 50 0108" }, { name: "Майя Шах", email: "maya.shah@zenithbrands.ca", phone: "+1 416 555 0109" }, { name: "Арман Петросян", email: "arman.petrosyan@ararat-electronics.am", phone: "+374 10 555110" }, { name: "София Мартинес", email: "sophia.martinez@sunlinehg.com", phone: "+1 555 120 0111" }, { name: "Лео Новак", email: "leo.novak@centralbuild.de", phone: "+49 30 5550 0112" }, { name: "Айла Грант", email: "isla.grant@blueharbor.co.uk", phone: "+44 161 555 0113" }, { name: "Матео Росси", email: "mateo.rossi@milanofh.it", phone: "+39 02 5550 0114" }, { name: "Нина Волкова", email: "nina.volkova@polaragri.kz", phone: "+7 727 555 0115" }, { name: "Итан Пак", email: "ethan.park@vertexcomponents.kr", phone: "+82 2 555 0116" }, { name: "Зара Хан", email: "zara.khan@crescentretail.ae", phone: "+971 2 555 0117" }, { name: "Уго Силва", email: "hugo.silva@lusois.pt", phone: "+351 21 555 0118" }, { name: "Хлоя Бернар", email: "chloe.bernard@santex.fr", phone: "+33 4 55 50 0119" }, { name: "Джеймс Уокер", email: "james.walker@metrowholesale.com", phone: "+1 555 120 0120" }, ]; return prospects.map((p, idx) => { const female = idx % 2 === 0; const picIdx = (idx % 70) + 1; return { teamId, name: p.name, avatarUrl: `https://randomuser.me/api/portraits/${female ? "women" : "men"}/${picIdx}.jpg`, email: p.email, phone: p.phone, }; }); } async function main() { const passwordHash = hashPassword(LOGIN_PASSWORD); const user = await prisma.user.upsert({ where: { id: "demo-user" }, update: { phone: LOGIN_PHONE, passwordHash, name: LOGIN_NAME, email: "owner@clientsflow.local" }, create: { id: "demo-user", phone: LOGIN_PHONE, passwordHash, name: LOGIN_NAME, email: "owner@clientsflow.local", }, }); const team = await prisma.team.upsert({ where: { id: "demo-team" }, update: { name: "Connect Рабочее пространство" }, create: { id: "demo-team", name: "Connect Рабочее пространство" }, }); await prisma.teamMember.upsert({ where: { teamId_userId: { teamId: team.id, userId: user.id } }, update: { role: "OWNER" }, create: { teamId: team.id, userId: user.id, role: "OWNER" }, }); const conversation = await prisma.aiConversation.upsert({ where: { id: `pilot-${team.id}` }, update: { title: "Пилот" }, create: { id: `pilot-${team.id}`, teamId: team.id, createdByUserId: user.id, title: "Пилот" }, }); await prisma.$transaction([ prisma.feedCard.deleteMany({ where: { teamId: team.id } }), prisma.contactPin.deleteMany({ where: { teamId: team.id } }), prisma.workspaceDocument.deleteMany({ where: { teamId: team.id } }), prisma.deal.deleteMany({ where: { teamId: team.id } }), prisma.calendarEvent.deleteMany({ where: { teamId: team.id } }), prisma.contactMessage.deleteMany({ where: { contact: { teamId: team.id } } }), prisma.aiMessage.deleteMany({ where: { teamId: team.id, conversationId: conversation.id } }), prisma.omniMessage.deleteMany({ where: { teamId: team.id } }), prisma.omniThread.deleteMany({ where: { teamId: team.id } }), prisma.omniContactIdentity.deleteMany({ where: { teamId: team.id } }), prisma.telegramBusinessConnection.deleteMany({ where: { teamId: team.id } }), prisma.contact.deleteMany({ where: { teamId: team.id } }), ]); const contacts = await prisma.contact.createManyAndReturn({ data: buildOdooAiContacts(team.id), select: { id: true, name: true }, }); const integrationModules = [ "Продажи + CRM + копилот прогнозирования", "Склад + прогноз спроса", "Закупки + оценка рисков поставщиков", "Бухгалтерия + AI-детекция аномалий", "Поддержка + ассистент триажа заявок", "Производство + AI-планирование мощностей", ]; await prisma.contactNote.createMany({ data: contacts.map((c, idx) => ({ contactId: c.id, content: `${c.name} рассматривает внедрение Odoo с AI-расширениями. ` + `Основной контур интеграции: ${integrationModules[idx % integrationModules.length]}. ` + `Ключевой драйвер покупки: сократить ручные операции и ускорить цикл принятия решений. ` + `Следующая веха: провести сессию уточнения, согласовать владельцев данных и утвердить KPI пилота.`, })), }); const channels = ["TELEGRAM", "WHATSAPP", "INSTAGRAM", "EMAIL"]; const contactMessages = []; for (let i = 0; i < contacts.length; i += 1) { const contact = contacts[i]; const base = atOffset(-(i % 18), 9 + (i % 7), (i * 7) % 60); contactMessages.push({ contactId: contact.id, kind: "MESSAGE", direction: "IN", channel: channels[i % channels.length], content: `Здравствуйте! Мы рассматриваем запуск Odoo + AI для ${contact.name}. Можем согласовать план интеграции на этой неделе?`, occurredAt: base, }); contactMessages.push({ contactId: contact.id, kind: "MESSAGE", direction: "OUT", channel: channels[(i + 1) % channels.length], content: "Да, предлагаю 45-минутный разбор: процессы, ограничения API и KPI пилота.", occurredAt: plusMinutes(base, 22), }); contactMessages.push({ contactId: contact.id, kind: "MESSAGE", direction: i % 3 === 0 ? "OUT" : "IN", channel: channels[(i + 2) % channels.length], content: "Обновление статуса: технический объём ясен; блокер — согласование бюджета и анкета по безопасности.", occurredAt: plusMinutes(base, 65), }); if (i % 3 === 0) { contactMessages.push({ contactId: contact.id, kind: "CALL", direction: "OUT", channel: "PHONE", content: "Созвон по уточнению: модули Odoo, потоки данных и AI-сценарии", audioUrl: "/audio-samples/national-road-9.m4a", durationSec: 180 + ((i * 23) % 420), occurredAt: plusMinutes(base, 110), }); } } await prisma.contactMessage.createMany({ data: contactMessages }); await prisma.calendarEvent.createMany({ data: contacts.flatMap((c, idx) => { // Историческая неделя до 20 Feb 2026: все сидовые встречи завершены. const firstStart = atOffset(-6 + (idx % 5), 10 + (idx % 6), (idx * 5) % 60); const secondStart = atOffset(-5 + (idx % 5), 14 + (idx % 4), (idx * 3) % 60); return [ { teamId: team.id, contactId: c.id, title: `Сессия уточнения: Odoo + AI с ${c.name}`, startsAt: firstStart, endsAt: plusMinutes(firstStart, 30), note: "Подтвердить рамки интеграции, текущий стек и метрики успеха пилота.", }, { teamId: team.id, contactId: c.id, title: `Архитектурный воркшоп: ${c.name}`, startsAt: secondStart, endsAt: plusMinutes(secondStart, 45), note: "Проверить маппинг API, границы ETL и ограничения для AI-ассистента.", }, ]; }), }); const stages = ["Лид", "Уточнение", "Подбор решения", "Коммерческое предложение", "Переговоры", "Пилот", "Проверка договора"]; for (const [idx, c] of contacts.entries()) { const nextStepText = idx % 4 === 0 ? "Отправить предложение по пилоту и зафиксировать список задач интеграции." : "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем."; const deal = await prisma.deal.create({ data: { teamId: team.id, contactId: c.id, title: `${c.name}: интеграция Odoo + AI`, stage: stages[idx % stages.length], amount: 18000 + (idx % 8) * 7000, nextStep: nextStepText, summary: "Потенциальная сделка на поэтапное внедрение Odoo с AI-копилотами для операций, продаж и планирования. " + "Коммерческая модель: уточнение + пилот + тиражирование.", }, select: { id: true }, }); const dueBase = atOffset((idx % 5) + 1, 11 + (idx % 4), 0); const steps = [ { dealId: deal.id, title: "Собрать уточняющие требования", description: "Подтвердить модули Odoo, владельцев данных и критерии успеха.", status: "done", order: 1, completedAt: atOffset(-2 - (idx % 3), 16, 0), dueAt: atOffset(-1, 12, 0), }, { dealId: deal.id, title: "Провести воркшоп по решению", description: "Согласовать границы интеграции и план пилота.", status: idx % 3 === 0 ? "in_progress" : "todo", order: 2, dueAt: dueBase, }, { dealId: deal.id, title: "Согласовать и отправить договор", description: "Выслать договор и зафиксировать дату подписи.", status: "todo", order: 3, dueAt: atOffset((idx % 5) + 6, 15, 0), }, ]; await prisma.dealStep.createMany({ data: steps }); const current = await prisma.dealStep.findFirst({ where: { dealId: deal.id, status: { not: "done" } }, orderBy: [{ order: "asc" }, { createdAt: "asc" }], select: { id: true }, }); await prisma.deal.update({ where: { id: deal.id }, data: { currentStepId: current?.id ?? null }, }); } await prisma.contactPin.createMany({ data: contacts.map((c, idx) => ({ teamId: team.id, contactId: c.id, text: idx % 3 === 0 ? "Уточнить владельца ERP, владельца данных и целевой квартал запуска." : "Держать коммуникацию вокруг одного KPI и следующего шага.", })), }); const proposalKeys = ["create_followup", "open_comm", "call", "draft_message", "run_summary", "prepare_question"]; await prisma.feedCard.createMany({ data: contacts .filter((_, idx) => idx % 3 === 0) .slice(0, 80) .map((c, idx) => ({ teamId: team.id, contactId: c.id, happenedAt: atOffset(-(idx % 6), 9 + (idx % 8), (idx * 9) % 60), text: `Я проверил активность по аккаунту ${c.name} в рамках сделки Odoo + AI. ` + "Есть достаточный импульс, чтобы перевести сделку на следующий этап при чётком следующем шаге.", proposalJson: { title: idx % 2 === 0 ? "Назначить созвон по рамкам пилота" : "Отправить сообщение для разблокировки у владельца бюджета", details: [ `Контакт: ${c.name}`, idx % 2 === 0 ? "Когда: на этой неделе, 45 минут" : "Когда: сегодня в основном канале", "Цель: подтвердить объём, владельца и следующую коммерческую контрольную точку", ], key: proposalKeys[idx % proposalKeys.length], }, })), }); await prisma.workspaceDocument.createMany({ data: [ { teamId: team.id, title: "Чеклист уточнения для интеграции Odoo", type: "Regulation", owner: "Команда решений", scope: "Предпродажное уточнение", summary: "Обязательные вопросы перед оценкой запуска Odoo + AI.", body: "## Нужно зафиксировать\n- Текущие модули ERP\n- Точки интеграции\n- Владельца данных по каждому домену\n- Ограничения безопасности\n- Базовые KPI пилота", updatedAt: atOffset(-1, 11, 10), }, { teamId: team.id, title: "Плейбук AI-копилота для Odoo", type: "Playbook", owner: "Лид AI-практики", scope: "Квалификация сценариев", summary: "Как позиционировать прогнозирование, ассистента и детекцию аномалий.", body: "## Поток\n1. Боль процесса\n2. Качество данных\n3. Целевая модель\n4. KPI успеха\n5. Объём пилота", updatedAt: atOffset(-2, 15, 0), }, { teamId: team.id, title: "Матрица цен для пилота", type: "Policy", owner: "Коммерческие операции", scope: "Контракты уточнения и пилота", summary: "Диапазоны цен для уточнения, пилота и продуктивной фазы.", body: "## Типовые диапазоны\n- Уточнение: 5k-12k\n- Пилот: 15k-45k\n- Тиражирование: 50k+\n\nВсегда привязывай стоимость к объёму и срокам.", updatedAt: atOffset(-3, 9, 30), }, { teamId: team.id, title: "Шаблон по безопасности и комплаенсу", type: "Template", owner: "Офис внедрения", scope: "Крупные клиенты", summary: "Шаблон ответов по data residency, RBAC, аудиту и обработке PII.", body: "## Разделы\n- Модель хостинга\n- Контроль доступа\n- Логирование и аудит\n- Срок хранения данных\n- Реакция на инциденты", updatedAt: atOffset(-4, 13, 45), }, { teamId: team.id, title: "Референс интеграционной архитектуры", type: "Playbook", owner: "Архитектурная команда", scope: "Технические воркшопы", summary: "Референс-архитектура для коннекторов Odoo, ETL и AI-сервисного слоя.", body: "## Слои\n- Базовые модули Odoo\n- Интеграционная шина\n- Хранилище данных\n- Эндпоинты AI-сервиса\n- Мониторинг", updatedAt: atOffset(-5, 10, 0), }, { teamId: team.id, title: "Чеклист готовности к запуску", type: "Regulation", owner: "PMO", scope: "Переход от пилота к продакшену", summary: "Чеклист перехода от приёмки пилота к запуску в прод.", body: "## Обязательно\n- KPI пилота утверждены\n- Backlog тиражирования приоритизирован\n- Владельцы назначены\n- Модель поддержки определена", updatedAt: atOffset(-6, 16, 15), }, ], }); console.log("Seed completed."); console.log(`Login phone: ${LOGIN_PHONE}`); console.log(`Login password: ${LOGIN_PASSWORD}`); console.log(`Team: ${team.name}`); console.log(`Contacts created: ${contacts.length}`); } main() .catch((e) => { console.error(e); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });