Files
clientsflow/frontend/prisma/seed.mjs
Ruslan Bakiev 6291797bb6 chore: upgrade Prisma 7, LangChain 1.x, Tailwind 4.2, Vue 3.5.29 and other deps
- Prisma 6 → 7: new prisma-client generator, prisma.config.ts, PrismaPg adapter, updated all imports
- LangChain 0.x → 1.x: @langchain/core, langgraph, openai
- Tailwind 4.1 → 4.2.1, daisyUI 5.5.19, Vue 3.5.29, ai 6.0.99, zod 4.3.6
- Fix MessageDirection bug in crm-updates.ts (OUTBOUND → OUT)
- Add server/generated to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:27:26 +07:00

423 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});