import { PrismaClient } from "@prisma/client"; 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") { process.env[key] = val; continue; } if (!process.env[key]) process.env[key] = val; } } loadEnvFromDotEnv(); const prisma = new PrismaClient(); const LOGIN_PHONE = "+15550000001"; const LOGIN_PASSWORD = "ConnectFlow#2026"; const LOGIN_NAME = "Connect Owner"; 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(); 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 buildContacts(teamId, count) { const firstNames = [ "Alex", "Mia", "Leo", "Sofia", "Noah", "Emma", "Liam", "Ava", "Ethan", "Luna", "Mason", "Chloe", "Logan", "Mila", "Lucas", "Nora", "Elijah", "Zoey", "James", "Aria", "Daniel", "Nina", "Henry", "Layla", "Oliver", "Iris", "Oscar", "Diana", "Max", "Eva", ]; const lastNames = [ "Carter", "Meyer", "Ali", "Petrov", "Rivera", "Ivanova", "Fisher", "Khan", "Wright", "Cole", "Silva", "Morris", "King", "Anderson", "Lopez", "Walker", "Young", "Scott", "Green", "Parker", ]; const companies = [ "Northline", "Connecta", "Volta", "Blueport", "Skyline", "PrimeGrid", "Helio", "CoreLabs", "NovaTrade", "Astera", ]; const locations = [ { country: "USA", city: "New York" }, { country: "USA", city: "Austin" }, { country: "Germany", city: "Berlin" }, { country: "UAE", city: "Dubai" }, { country: "Spain", city: "Barcelona" }, { country: "Armenia", city: "Yerevan" }, { country: "UK", city: "London" }, { country: "France", city: "Paris" }, { country: "Singapore", city: "Singapore" }, { country: "Canada", city: "Toronto" }, ]; const rows = []; for (let i = 0; i < count; i += 1) { const first = firstNames[i % firstNames.length]; const last = lastNames[Math.floor(i / firstNames.length) % lastNames.length]; const company = `${companies[i % companies.length]} ${String.fromCharCode(65 + (i % 26))}`; const loc = locations[i % locations.length]; const female = i % 2 === 0; const picIdx = (i % 70) + 1; rows.push({ teamId, name: `${first} ${last} ${i + 1}`, company, country: loc.country, location: loc.city, avatarUrl: `https://randomuser.me/api/portraits/${female ? "women" : "men"}/${picIdx}.jpg`, email: `${first.toLowerCase()}.${last.toLowerCase()}${i + 1}@${company.toLowerCase().replace(/\s+/g, "")}.example`, phone: `+1 555 01${String(i).padStart(4, "0")}`, }); } return rows; } 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 Workspace" }, create: { id: "demo-team", name: "Connect Workspace" }, }); 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.chatConversation.upsert({ where: { id: `pilot-${team.id}` }, update: { title: "Pilot" }, create: { id: `pilot-${team.id}`, teamId: team.id, createdByUserId: user.id, title: "Pilot" }, }); 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.chatMessage.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: buildContacts(team.id, 220), select: { id: true, name: true, company: true }, }); await prisma.contactNote.createMany({ data: contacts.map((c, idx) => ({ contactId: c.id, content: `Summary for ${c.name}. Main objective: move the account to a predictable weekly rhythm. ` + `Current context: ${c.company ?? "Account"} is active in at least one channel. ` + `Recommended path: keep messages short, lock a concrete next step, and update the note after each interaction. ` + `Priority signal ${idx % 5 === 0 ? "high" : "normal"}.`, })), }); 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: `Hi, this is ${contact.name}. Can we sync on timeline this week?`, occurredAt: base, }); contactMessages.push({ contactId: contact.id, kind: "MESSAGE", direction: "OUT", channel: channels[(i + 1) % channels.length], content: `Sure. I suggest two slots and a clear agenda.`, occurredAt: plusMinutes(base, 22), }); contactMessages.push({ contactId: contact.id, kind: "MESSAGE", direction: i % 3 === 0 ? "OUT" : "IN", channel: channels[(i + 2) % channels.length], content: `Status update: legal owner and decision date are the two blockers now.`, occurredAt: plusMinutes(base, 65), }); if (i % 4 === 0) { contactMessages.push({ contactId: contact.id, kind: "CALL", direction: "OUT", channel: "PHONE", content: "Voice call from CRM", durationSec: 180 + ((i * 23) % 420), transcriptJson: [ `${contact.name}: We need a clear owner for approval.`, `You: Agreed, let's lock this today and set the next checkpoint.`, ], occurredAt: plusMinutes(base, 110), }); } } await prisma.contactMessage.createMany({ data: contactMessages }); await prisma.calendarEvent.createMany({ data: contacts.flatMap((c, idx) => { const firstStart = atOffset((idx % 21) - 3, 10 + (idx % 6), (idx * 5) % 60); const secondStart = atOffset((idx % 28) + 1, 14 + (idx % 4), (idx * 3) % 60); return [ { teamId: team.id, contactId: c.id, title: `Follow-up with ${c.name}`, startsAt: firstStart, endsAt: plusMinutes(firstStart, 30), note: "Confirm owner, timeline, and next concrete action.", status: "planned", }, { teamId: team.id, contactId: c.id, title: `Checkpoint: ${c.company ?? c.name}`, startsAt: secondStart, endsAt: plusMinutes(secondStart, 45), note: "Review progress and unblock pending decisions.", status: idx % 6 === 0 ? "done" : "planned", }, ]; }), }); const stages = ["Qualification", "Proposal", "Negotiation", "Contract"]; await prisma.deal.createMany({ data: contacts .filter((_, idx) => idx % 5 !== 0) .map((c, idx) => ({ teamId: team.id, contactId: c.id, title: `${c.company ?? "Account"} expansion`, stage: stages[idx % stages.length], amount: 8000 + (idx % 17) * 1500, nextStep: "Lock next sync and owner on client side.", summary: "Deal is active. Focus on speed and explicit decision checkpoints.", })), }); await prisma.contactPin.createMany({ data: contacts.map((c, idx) => ({ teamId: team.id, contactId: c.id, text: idx % 3 === 0 ? "Pinned: calendar event is near, prepare a concise follow-up note." : "Pinned: keep one explicit ask in each message.", })), }); 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: `I analyzed the latest contact activity for ${c.name}. ` + `There is enough momentum to push one concrete action now and reduce response latency.`, proposalJson: { title: idx % 2 === 0 ? "Add focused follow-up event" : "Draft a concise unblock message", details: [ `Contact: ${c.name}`, idx % 2 === 0 ? "Timing: in the next 60 minutes" : "Timing: send in the primary active channel", "Goal: lock owner and next exact date", ], key: proposalKeys[idx % proposalKeys.length], }, })), }); await prisma.workspaceDocument.createMany({ data: [ { teamId: team.id, title: "Response time regulation", type: "Regulation", owner: "Revenue Ops", scope: "All active deals", summary: "Rules for first response and follow-up SLA across channels.", body: "## SLA\n- First response in under 30 minutes for active threads.\n- Escalate if no owner is assigned.\n\n## Rule\nAlways end a message with one explicit next step.", updatedAt: atOffset(-1, 11, 10), }, { teamId: team.id, title: "Discovery playbook", type: "Playbook", owner: "Sales Lead", scope: "Discovery and qualification", summary: "Consistent structure for discovery calls and follow-up notes.", body: "## Flow\n1. Pain\n2. Impact\n3. Owner\n4. Timeline\n5. Next step\n\n## Output\nStore concise summary in the contact card.", updatedAt: atOffset(-2, 15, 0), }, { teamId: team.id, title: "AI action policy", type: "Policy", owner: "Founders", scope: "AI recommendations", summary: "What can be auto-drafted and what always needs explicit approval.", body: "## Allowed\n- Draft suggestions\n- Summaries\n\n## Requires approval\n- Outbound send\n- Event creation\n- Deal stage change", updatedAt: atOffset(-3, 9, 30), }, { teamId: team.id, title: "Post-call template", type: "Template", owner: "Enablement", scope: "Any completed call", summary: "Template for short post-call summary with owners and deadlines.", body: "## Template\n- Aligned\n- Open items\n- Owner per action\n- Next date\n\nKeep it under 6 lines.", updatedAt: atOffset(-4, 13, 45), }, { teamId: team.id, title: "Objection handling map", type: "Playbook", owner: "Commercial", scope: "Late-stage objections", summary: "Common objections and concise response strategy.", body: "## Objections\n- Timing\n- Budget\n- Legal\n\n## Response\nAcknowledge, clarify owner, set a concrete checkpoint.", updatedAt: atOffset(-5, 10, 0), }, { teamId: team.id, title: "Pipeline hygiene", type: "Regulation", owner: "Operations", scope: "Pipeline updates", summary: "Minimal mandatory updates after each interaction.", body: "## Required\n- Last touch timestamp\n- Next step\n- Risk marker\n\nNo long forms, only concise text.", updatedAt: atOffset(-6, 16, 15), }, ], }); await prisma.chatMessage.createMany({ data: [ { teamId: team.id, conversationId: conversation.id, authorUserId: null, role: "ASSISTANT", text: "Workspace is ready. I connected contacts, communications, calendar, deals, and recommendations.", planJson: { steps: ["Open any contact", "Review chat + pinned tasks", "Confirm next event or message"], tools: ["contacts", "communications", "calendar", "feed"], }, }, { teamId: team.id, conversationId: conversation.id, authorUserId: null, role: "ASSISTANT", text: "Dataset loaded with 220 contacts and linked timeline activity.", planJson: { steps: ["Filter by country/company", "Open active threads", "Apply one recommendation"], tools: ["search", "pins"] }, }, ], }); 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(); });