import { PrismaClient } from "@prisma/client"; import fs from "node:fs"; import path from "node:path"; 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; // Force DATABASE_URL from local .env for scripts, to avoid inheriting a stale shell env. if (key === "DATABASE_URL") { process.env[key] = val; continue; } if (!process.env[key]) process.env[key] = val; } } loadEnvFromDotEnv(); const prisma = new PrismaClient(); function atOffset(days, hour, minute) { const d = new Date(); d.setDate(d.getDate() + days); d.setHours(hour, minute, 0, 0); return d; } async function main() { // Create default team/user for dev. const user = await prisma.user.upsert({ where: { id: "demo-user" }, update: { email: "demo@clientsflow.local", name: "Demo User" }, create: { id: "demo-user", email: "demo@clientsflow.local", name: "Demo User" }, }); const team = await prisma.team.upsert({ where: { id: "demo-team" }, update: { name: "Demo Team" }, create: { id: "demo-team", name: "Demo Team" }, }); await prisma.teamMember.upsert({ where: { teamId_userId: { teamId: team.id, userId: user.id } }, update: {}, create: { teamId: team.id, userId: user.id, role: "OWNER" }, }); // Idempotent-ish seed per team: if we already have contacts in this team, do nothing. const existing = await prisma.contact.count({ where: { teamId: team.id } }); if (existing > 0) return; const contacts = await prisma.contact.createManyAndReturn({ data: [ { teamId: team.id, name: "Anna Meyer", company: "Nordline GmbH", country: "Germany", location: "Berlin", avatarUrl: "https://randomuser.me/api/portraits/women/44.jpg", email: "anna@nordline.example", phone: "+49 30 123 45 67", }, { teamId: team.id, name: "Murat Ali", company: "Connect FZCO", country: "UAE", location: "Dubai", avatarUrl: "https://randomuser.me/api/portraits/men/32.jpg", email: "murat@connect.example", phone: "+971 50 123 4567", }, { teamId: team.id, name: "Ilya Petroff", company: "Volta Tech", country: "Armenia", location: "Yerevan", avatarUrl: "https://randomuser.me/api/portraits/men/18.jpg", email: "ilya@volta.example", phone: "+374 10 123 456", }, { teamId: team.id, name: "Carlos Rivera", company: "BluePort", country: "Spain", location: "Barcelona", avatarUrl: "https://randomuser.me/api/portraits/men/65.jpg", email: "carlos@blueport.example", phone: "+34 600 123 456", }, { teamId: team.id, name: "Daria Ivanova", company: "Skyline Trade", country: "Kazakhstan", location: "Almaty", avatarUrl: "https://randomuser.me/api/portraits/women/22.jpg", email: "daria@skyline.example", phone: "+7 777 123 45 67", }, ], }); const byName = Object.fromEntries(contacts.map((c) => [c.name, c])); await prisma.contactNote.createMany({ data: [ { contactId: byName["Anna Meyer"].id, content: "Decision owner. Prefers short, concrete updates with a clear next step.\nRisk: decision date slips if we don't lock timeline.", }, { contactId: byName["Murat Ali"].id, content: "High activity. Needs legal path clarity and an explicit owner on their side.\nBest move: lock legal owner + target signature date.", }, { contactId: byName["Ilya Petroff"].id, content: "Early-stage. Wants structured onboarding before commercial details.\nBest move: onboarding plan + 2 time slots.", }, ], }); await prisma.contactMessage.createMany({ data: [ { contactId: byName["Anna Meyer"].id, kind: "MESSAGE", direction: "IN", channel: "TELEGRAM", content: "Thanks for the demo. Can you send 2 pricing options?", occurredAt: atOffset(0, 10, 20), }, { contactId: byName["Anna Meyer"].id, kind: "MESSAGE", direction: "OUT", channel: "EMAIL", content: "Sure. Option A/B attached. Can you confirm decision date for this cycle?", occurredAt: atOffset(0, 10, 35), }, { contactId: byName["Murat Ali"].id, kind: "MESSAGE", direction: "IN", channel: "WHATSAPP", content: "Let's do a quick call. Need to clarify legal owner.", occurredAt: atOffset(-1, 18, 10), }, { contactId: byName["Ilya Petroff"].id, kind: "MESSAGE", direction: "OUT", channel: "EMAIL", content: "Draft: onboarding plan + two slots for tomorrow.", occurredAt: atOffset(-1, 11, 12), }, { contactId: byName["Murat Ali"].id, kind: "CALL", direction: "OUT", channel: "PHONE", content: "Call started from CRM", durationSec: 180, occurredAt: atOffset(-1, 18, 30), }, ], }); await prisma.calendarEvent.createMany({ data: [ { teamId: team.id, contactId: byName["Anna Meyer"].id, title: "Follow-up: Anna", startsAt: atOffset(0, 12, 30), endsAt: atOffset(0, 13, 0), note: "Lock decision date + confirm option A/B.", status: "planned", }, { teamId: team.id, contactId: byName["Murat Ali"].id, title: "Call: Murat (legal owner)", startsAt: atOffset(0, 15, 0), endsAt: atOffset(0, 15, 20), note: "Confirm legal owner + target signature date.", status: "planned", }, ], }); await prisma.deal.createMany({ data: [ { teamId: team.id, contactId: byName["Anna Meyer"].id, title: "Nordline onboarding", stage: "Proposal", amount: 25000, nextStep: "Lock decision date", summary: "After demo: pricing options sent; waiting for decision date.", }, { teamId: team.id, contactId: byName["Murat Ali"].id, title: "Connect legal alignment", stage: "Qualification", amount: 18000, nextStep: "Confirm legal owner", summary: "High engagement; needs legal owner on their side.", }, ], }); await prisma.chatConversation.upsert({ where: { id: `pilot-${team.id}` }, update: {}, create: { id: `pilot-${team.id}`, teamId: team.id, createdByUserId: user.id, title: "Pilot", }, }); await prisma.chatMessage.createMany({ data: [ { teamId: team.id, conversationId: `pilot-${team.id}`, authorUserId: null, role: "ASSISTANT", text: "Я смотрю календарь, контакты и переписки как один поток. Спроси: \"чем заняться сегодня\" или \"покажи 10 лучших клиентов\".", planJson: { steps: ["Скажи задачу", "Я соберу срез данных", "Предложу план и действия"], tools: ["read index/contacts.json"] }, }, ], }); await prisma.contactPin.createMany({ data: [ { teamId: team.id, contactId: byName["Anna Meyer"].id, text: "First lock the decision date, then send the final offer." }, { teamId: team.id, contactId: byName["Anna Meyer"].id, text: "A short follow-up is needed no later than 30 minutes after the demo." }, { teamId: team.id, contactId: byName["Murat Ali"].id, text: "In every update, confirm the legal owner on the client side." }, { teamId: team.id, contactId: byName["Ilya Petroff"].id, text: "Work through a structured onboarding plan, not pricing first." }, ], }); await prisma.workspaceDocument.createMany({ data: [ { teamId: team.id, title: "Outbound cadence v1", type: "Regulation", owner: "Revenue Ops", scope: "All B2B accounts", summary: "Unified sequence for first touch, follow-up, and qualification.", body: "## Goal\nMove a new contact to the first qualified call within 5 business days.\n\n## Base sequence\n- Day 0: first message in the primary channel.\n- Day 1: short follow-up with one clear ask.\n- Day 3: second follow-up + alternate channel.\n- Day 5: final ping and move to \"later\".\n\n## Rules\n- Always keep one explicit next step in each message.\n- Avoid long text walls.\n- After each reply, update context in the contact card.", updatedAt: atOffset(-1, 10, 0), }, { teamId: team.id, title: "Discovery call playbook", type: "Playbook", owner: "Sales Lead", scope: "Discovery calls", summary: "Call structure, mandatory questions, and outcome logging format.", body: "## Structure\n1. Context check (2 min)\n2. Current pain (8 min)\n3. Success criteria (6 min)\n4. Next step lock (4 min)\n\n## Mandatory outcomes\n- Confirmed business owner\n- Confirmed decision timeline\n- Confirmed next meeting date\n\n## Notes format\nAlways log: pain, impact, owner, ETA, risks.", updatedAt: atOffset(-2, 12, 15), }, { teamId: team.id, title: "AI assistant operating policy", type: "Policy", owner: "Founders", scope: "AI recommendations and automations", summary: "What actions AI can suggest and what requires explicit approval.", body: "## Allowed without approval\n- Draft message suggestions\n- Calendar proposal suggestions\n- Conversation summaries\n\n## Requires explicit approval\n- Sending a message to an external contact\n- Creating a calendar event\n- Changing deal stage\n\n## Logging\nEvery AI action must leave a short trace in the feed.", updatedAt: atOffset(-3, 9, 40), }, { teamId: team.id, title: "Post-call follow-up template", type: "Template", owner: "Enablement", scope: "Any completed client call", summary: "Template for short post-call follow-up with aligned actions.", body: "## Message template\nThanks for the call. Summary below:\n- What we aligned on\n- What remains open\n- Owner per action\n- Exact date for next sync\n\n## Quality bar\nThe client should understand the next step within 10 seconds.", updatedAt: atOffset(-4, 16, 20), }, ], }); await prisma.feedCard.createMany({ data: [ { teamId: team.id, contactId: byName["Anna Meyer"].id, happenedAt: atOffset(0, 9, 35), text: "I analyzed Anna Meyer's latest activity: after a demo, the decision window is usually open for 1-2 hours. I suggest scheduling a follow-up immediately to keep momentum.", proposalJson: { title: "Add event to calendar", details: ["Contact: Anna Meyer", "Start: 30 minutes from now", "Duration: 30 minutes"], key: "create_followup", }, }, { teamId: team.id, contactId: byName["Murat Ali"].id, happenedAt: atOffset(0, 10, 8), text: "I found that Murat Ali gave 3 quick replies in a row over the last hour. I suggest moving to a short call now while engagement is high.", proposalJson: { title: "Start a call and open chat", details: ["Contact: Murat Ali", "Channel: Phone", "After action: open the communication thread for this contact"], key: "call", }, }, ], }); } main() .catch((e) => { console.error(e); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });