import { PrismaClient } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { brand } from '@mento/shared'; const prisma = new PrismaClient(); const DEFAULT_STAGES = [ { name: 'سرنخ', isWon: false, isLost: false }, { name: 'تماس اول', isWon: false, isLost: false }, { name: 'پیشنهاد', isWon: false, isLost: false }, { name: 'مذاکره', isWon: false, isLost: false }, { name: 'برنده', isWon: true, isLost: false }, { name: 'از دست‌رفته', isWon: false, isLost: true }, ] as const; async function seedPlatformAdmin() { const envMobile = (process.env.PLATFORM_ADMIN_MOBILE ?? '').trim(); const envEmail = (process.env.PLATFORM_ADMIN_EMAIL ?? '').trim().toLowerCase(); const envPassword = process.env.PLATFORM_ADMIN_PASSWORD ?? ''; const envName = (process.env.PLATFORM_ADMIN_NAME ?? 'سوپرادمین').trim() || 'سوپرادمین'; const isProd = process.env.NODE_ENV === 'production'; const mobileRaw = envMobile || (isProd ? '' : '09120000000'); const password = envPassword || (isProd ? '' : 'demo1234'); const email = envEmail || null; if (!mobileRaw || !password) { console.log( 'Platform admin seed skipped (set PLATFORM_ADMIN_MOBILE and PLATFORM_ADMIN_PASSWORD)', ); return; } if (password.length < 8) { console.log('Platform admin seed skipped (password must be at least 8 chars)'); return; } // normalize: 09xxxxxxxxx let mobile = mobileRaw.replace(/[\s\-()]/g, ''); if (mobile.startsWith('+98')) mobile = `0${mobile.slice(3)}`; if (mobile.startsWith('98') && mobile.length === 12) mobile = `0${mobile.slice(2)}`; if (/^9\d{9}$/.test(mobile)) mobile = `0${mobile}`; if (!/^09\d{9}$/.test(mobile)) { console.log('Platform admin seed skipped (PLATFORM_ADMIN_MOBILE invalid)'); return; } const passwordHash = await bcrypt.hash(password, 10); const existingByMobile = await prisma.platformAdmin.findUnique({ where: { mobile } }); if (existingByMobile) { await prisma.platformAdmin.update({ where: { id: existingByMobile.id }, data: { passwordHash, fullName: envName, isActive: true, ...(email ? { email } : {}), }, }); console.log('Platform admin password refreshed:', mobile); return; } if (email) { const existingByEmail = await prisma.platformAdmin.findUnique({ where: { email } }); if (existingByEmail) { await prisma.platformAdmin.update({ where: { id: existingByEmail.id }, data: { mobile, passwordHash, fullName: envName, isActive: true, }, }); console.log('Platform admin linked mobile + password refreshed:', mobile, '→', email); return; } } const orphans = await prisma.platformAdmin.findMany({ where: { mobile: null }, take: 2, }); if (orphans.length === 1) { const orphan = orphans[0]!; await prisma.platformAdmin.update({ where: { id: orphan.id }, data: { mobile, passwordHash, fullName: envName, isActive: true, ...(email ? { email } : {}), }, }); console.log('Platform admin orphan linked mobile:', mobile); return; } await prisma.platformAdmin.create({ data: { mobile, email, passwordHash, fullName: envName, }, }); console.log('Seeded platform admin:', mobile); } async function main() { await seedPlatformAdmin(); const email = 'owner@demo.local'; let org = await prisma.organization.findUnique({ where: { slug: 'demo-mento' } }); if (!org) { org = await prisma.organization.findUnique({ where: { slug: 'demo-ment' } }); if (org) { org = await prisma.organization.update({ where: { id: org.id }, data: { slug: 'demo-mento', name: `سازمان نمونه ${brand.nameFa}` }, }); console.log('Renamed demo org slug to demo-mento'); } } if (!org) { const passwordHash = await bcrypt.hash('demo1234', 10); org = await prisma.organization.create({ data: { name: `سازمان نمونه ${brand.nameFa}`, slug: 'demo-mento', baseCurrency: 'IRR', enabledCurrencies: ['IRR', 'USD', 'EUR'], businessField: 'tech', users: { create: { email, mobile: '09120000001', fullName: 'مدیر نمونه', passwordHash, role: 'owner', emailVerifiedAt: new Date(), mobileVerifiedAt: new Date(), }, }, }, }); console.log('Seeded organization:', org.slug); console.log('Login:', email, '/ demo1234'); } else { console.log('Organization exists:', org.slug); } const owner = await prisma.user.findFirst({ where: { organizationId: org.id, email }, }); if (owner && !owner.mobile) { await prisma.user.update({ where: { id: owner.id }, data: { mobile: '09120000001', emailVerifiedAt: owner.emailVerifiedAt ?? new Date(), mobileVerifiedAt: owner.mobileVerifiedAt ?? new Date(), }, }); console.log('Updated demo owner mobile: 09120000001'); } // چندارزی نمونه await prisma.organization.update({ where: { id: org.id }, data: { baseCurrency: 'IRR', enabledCurrencies: ['IRR', 'USD', 'EUR'], businessField: 'tech', }, }); for (const row of [ { currency: 'USD', rateToBase: 920_000 }, { currency: 'EUR', rateToBase: 1_000_000 }, ] as const) { await prisma.exchangeRate.upsert({ where: { organizationId_currency: { organizationId: org.id, currency: row.currency, }, }, create: { organizationId: org.id, currency: row.currency, rateToBase: row.rateToBase, }, update: { rateToBase: row.rateToBase }, }); } const salesEmail = 'sales@demo.local'; const existingSales = await prisma.user.findFirst({ where: { organizationId: org.id, email: salesEmail }, }); if (!existingSales) { await prisma.user.create({ data: { organizationId: org.id, email: salesEmail, mobile: '09120000002', fullName: 'کارشناس فروش نمونه', passwordHash: await bcrypt.hash('demo1234', 10), role: 'sales', emailVerifiedAt: new Date(), mobileVerifiedAt: new Date(), }, }); console.log('Seeded sales user:', salesEmail, '/ demo1234'); } const adminEmail = 'admin@demo.local'; const existingAdmin = await prisma.user.findFirst({ where: { organizationId: org.id, email: adminEmail }, }); if (!existingAdmin) { await prisma.user.create({ data: { organizationId: org.id, email: adminEmail, mobile: '09120000003', fullName: 'مدیر سازمانی نمونه', passwordHash: await bcrypt.hash('demo1234', 10), role: 'admin', emailVerifiedAt: new Date(), mobileVerifiedAt: new Date(), }, }); console.log('Seeded admin user:', adminEmail, '/ demo1234'); } async function ensureMembership( userId: string, organizationId: string, role: 'owner' | 'admin' | 'sales' | 'support', ) { await prisma.membership.upsert({ where: { userId_organizationId: { userId, organizationId } }, create: { userId, organizationId, role }, update: { role }, }); } const ownerRow = await prisma.user.findFirst({ where: { email, organizationId: org.id }, }); if (ownerRow) await ensureMembership(ownerRow.id, org.id, 'owner'); const salesRow = await prisma.user.findFirst({ where: { email: salesEmail } }); if (salesRow) await ensureMembership(salesRow.id, org.id, 'sales'); const adminRow = await prisma.user.findFirst({ where: { email: adminEmail } }); if (adminRow) await ensureMembership(adminRow.id, org.id, 'admin'); let peopleCount = await prisma.person.count({ where: { organizationId: org.id }, }); if (peopleCount === 0) { const acme = await prisma.company.create({ data: { organizationId: org.id, name: 'شرکت نمونه آکمه', phone: '02188776655', email: 'info@acme.example', address: 'تهران، خیابان ولیعصر', notes: 'مشتری بالقوه سازمانی', }, }); const nova = await prisma.company.create({ data: { organizationId: org.id, name: 'نوا تجارت', phone: '03132221100', email: 'hello@nova.example', }, }); await prisma.person.createMany({ data: [ { organizationId: org.id, companyId: acme.id, firstName: 'سارا', lastName: 'محمدی', email: 'sara@acme.example', mobile: '09121234567', position: 'مدیر خرید', }, { organizationId: org.id, companyId: acme.id, firstName: 'علی', lastName: 'رضایی', email: 'ali@acme.example', mobile: '09129876543', position: 'کارشناس فروش', }, { organizationId: org.id, companyId: nova.id, firstName: 'مریم', lastName: 'کریمی', email: 'maryam@nova.example', mobile: '09351234567', position: 'مدیرعامل', }, { organizationId: org.id, firstName: 'حسین', lastName: 'نوری', email: 'hosein.nouri@example.com', mobile: '09121112233', notes: 'سرنخ فردی بدون شرکت', }, ], }); console.log('Seeded sample companies and people'); peopleCount = 4; } else { console.log('Contacts already seeded'); } let pipeline = await prisma.pipeline.findFirst({ where: { organizationId: org.id, isDefault: true }, include: { stages: { orderBy: { position: 'asc' } } }, }); if (!pipeline) { pipeline = await prisma.pipeline.create({ data: { organizationId: org.id, name: 'مسیر فروش', isDefault: true, stages: { create: DEFAULT_STAGES.map((s, index) => ({ name: s.name, position: index, isWon: s.isWon, isLost: s.isLost, })), }, }, include: { stages: { orderBy: { position: 'asc' } } }, }); console.log('Seeded default pipeline'); } else { console.log('Pipeline already seeded'); } const servicesPipeline = await prisma.pipeline.findFirst({ where: { organizationId: org.id, name: 'مسیر خدمات' }, }); if (!servicesPipeline) { await prisma.pipeline.create({ data: { organizationId: org.id, name: 'مسیر خدمات', isDefault: false, stages: { create: DEFAULT_STAGES.map((s, index) => ({ name: s.name, position: index, isWon: s.isWon, isLost: s.isLost, })), }, }, }); console.log('Seeded services pipeline'); } else { console.log('Services pipeline already seeded'); } // مسیر منتظر — مطابق طراحی UI (۳ مرحله + معاملات نمونه) const WAITING_STAGES = [ { name: 'پیگیری', isWon: false, isLost: false }, { name: 'مذاکرات اولیه', isWon: false, isLost: false }, { name: 'قیمت و نهایی کردن', isWon: false, isLost: false }, ] as const; const WAITING_DEALS: Array<{ stageName: (typeof WAITING_STAGES)[number]['name']; title: string; notes: string; amount: number; expectedCloseAt: string; position: number; }> = [ { stageName: 'پیگیری', title: 'نرم‌افزار حسابان', notes: 'نصب سیستم یکپارچه', amount: 250_000_000, expectedCloseAt: '2024-07-15T00:00:00.000Z', position: 0, }, { stageName: 'پیگیری', title: 'همکاران سیستم', notes: 'نسخه سازمانی', amount: 350_000_000, expectedCloseAt: '2024-07-08T00:00:00.000Z', position: 1, }, { stageName: 'پیگیری', title: 'بازرگانی امید', notes: 'کاربران سازمانی', amount: 180_000_000, expectedCloseAt: '2024-07-17T00:00:00.000Z', position: 2, }, { stageName: 'پیگیری', title: 'کالای صنعتی پارس', notes: 'ماژول انبارداری', amount: 120_000_000, expectedCloseAt: '2024-07-23T00:00:00.000Z', position: 3, }, { stageName: 'مذاکرات اولیه', title: 'فروشگاه تک‌کالا', notes: 'ماژول فروش', amount: 280_000_000, expectedCloseAt: '2024-07-13T00:00:00.000Z', position: 0, }, { stageName: 'مذاکرات اولیه', title: 'شرکت داده‌پرداز', notes: 'اتصال به حسابداری', amount: 190_000_000, expectedCloseAt: '2024-07-10T00:00:00.000Z', position: 1, }, { stageName: 'مذاکرات اولیه', title: 'گروه فنی مهندسی آوا', notes: 'ماژول پروژه‌ها', amount: 315_000_000, expectedCloseAt: '2024-07-05T00:00:00.000Z', position: 2, }, { stageName: 'مذاکرات اولیه', title: 'رستوران زنجیره‌ای نیک', notes: 'ماژول رستوران', amount: 165_000_000, expectedCloseAt: '2024-07-09T00:00:00.000Z', position: 3, }, { stageName: 'قیمت و نهایی کردن', title: 'شرکت تجارت', notes: 'یکپارچه‌سازی کامل', amount: 640_000_000, expectedCloseAt: '2024-06-30T00:00:00.000Z', position: 0, }, { stageName: 'قیمت و نهایی کردن', title: 'فروشگاه اینترنتی دیجی‌خونه', notes: 'فروشگاه اینترنتی', amount: 420_000_000, expectedCloseAt: '2024-06-28T00:00:00.000Z', position: 1, }, { stageName: 'قیمت و نهایی کردن', title: 'شرکت داروسازی بهداشت', notes: 'ماژول مالی و انبار', amount: 780_000_000, expectedCloseAt: '2024-07-02T00:00:00.000Z', position: 2, }, ]; let waitingPipeline = await prisma.pipeline.findFirst({ where: { organizationId: org.id, name: 'مسیر منتظر' }, include: { stages: { orderBy: { position: 'asc' } } }, }); if (!waitingPipeline) { waitingPipeline = await prisma.pipeline.create({ data: { organizationId: org.id, name: 'مسیر منتظر', isDefault: false, stages: { create: WAITING_STAGES.map((s, index) => ({ name: s.name, position: index, isWon: s.isWon, isLost: s.isLost, })), }, }, include: { stages: { orderBy: { position: 'asc' } } }, }); console.log('Seeded waiting pipeline (مسیر منتظر)'); } else { console.log('Waiting pipeline already seeded'); } const waitingDealCount = await prisma.deal.count({ where: { organizationId: org.id, pipelineId: waitingPipeline.id }, }); if (waitingDealCount === 0) { const stageByName = new Map( waitingPipeline.stages.map((s) => [s.name, s] as const), ); await prisma.deal.createMany({ data: WAITING_DEALS.map((d) => { const stage = stageByName.get(d.stageName); if (!stage) { throw new Error(`Missing stage ${d.stageName} on waiting pipeline`); } return { organizationId: org.id, pipelineId: waitingPipeline!.id, stageId: stage.id, title: d.title, notes: d.notes, amount: d.amount, currency: 'IRR', expectedCloseAt: new Date(d.expectedCloseAt), ownerId: owner?.id, position: d.position, tags: ['دمو-مسیر'], }; }), }); console.log('Seeded waiting pipeline deals:', WAITING_DEALS.length); } else { console.log('Waiting pipeline deals already seeded'); } const dealsCount = await prisma.deal.count({ where: { organizationId: org.id }, }); if (dealsCount === 0) { const people = await prisma.person.findMany({ where: { organizationId: org.id }, include: { company: true }, orderBy: { createdAt: 'asc' }, }); const stages = pipeline.stages; const lead = stages[0]; const call = stages[1]; const proposal = stages[2]; const negotiation = stages[3]; if (!lead || !call || !proposal || !negotiation || people.length === 0) { console.log('Skip deals seed: missing stages or people'); } else { await prisma.deal.createMany({ data: [ { organizationId: org.id, pipelineId: pipeline.id, stageId: lead.id, title: 'فروش بسته سازمانی آکمه', amount: 120_000_000, currency: 'IRR', source: 'معرفی', tags: ['سازمانی', 'اولویت‌بالا'], personId: people[0]?.id, companyId: people[0]?.companyId, ownerId: owner?.id, position: 0, }, { organizationId: org.id, pipelineId: pipeline.id, stageId: call.id, title: 'تمدید قرارداد نوا', amount: 45_000_000, currency: 'IRR', source: 'وب‌سایت', tags: ['تمدید'], isStale: true, personId: people[2]?.id, companyId: people[2]?.companyId, ownerId: owner?.id, position: 0, }, { organizationId: org.id, pipelineId: pipeline.id, stageId: proposal.id, title: 'مشاوره فردی حسین نوری', amount: 8_500_000, currency: 'IRR', source: 'تماس ورودی', tags: ['خرد'], personId: people[3]?.id, ownerId: owner?.id, position: 0, }, { organizationId: org.id, pipelineId: pipeline.id, stageId: negotiation.id, title: 'ارتقای لایسنس آکمه', amount: 70_000_000, currency: 'IRR', tags: ['ارتقاء'], isRotten: true, personId: people[1]?.id, companyId: people[1]?.companyId, ownerId: owner?.id, position: 0, }, ], }); console.log('Seeded sample deals'); } } else { console.log('Deals already seeded'); const sample = await prisma.deal.findFirst({ where: { organizationId: org.id, title: 'فروش بسته سازمانی آکمه' }, }); if (sample && sample.tags.length === 0) { await prisma.deal.update({ where: { id: sample.id }, data: { tags: ['سازمانی', 'اولویت‌بالا'] }, }); const stale = await prisma.deal.findFirst({ where: { organizationId: org.id, title: 'تمدید قرارداد نوا' }, }); if (stale) { await prisma.deal.update({ where: { id: stale.id }, data: { tags: ['تمدید'], isStale: true }, }); } const rotten = await prisma.deal.findFirst({ where: { organizationId: org.id, title: 'ارتقای لایسنس آکمه' }, }); if (rotten) { await prisma.deal.update({ where: { id: rotten.id }, data: { tags: ['ارتقاء'], isRotten: true }, }); } console.log('Enriched sample deals with tags/stale/rotten'); } } const activitiesCount = await prisma.activity.count({ where: { organizationId: org.id }, }); if (activitiesCount === 0) { const peopleForAct = await prisma.person.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const dealsForAct = await prisma.deal.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const now = new Date(); const todayMorning = new Date(now); todayMorning.setHours(10, 0, 0, 0); const todayAfternoon = new Date(now); todayAfternoon.setHours(15, 30, 0, 0); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(11, 0, 0, 0); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(12, 0, 0, 0); await prisma.activity.createMany({ data: [ { organizationId: org.id, type: 'call', title: 'تماس پیگیری با سارا محمدی', description: 'وضعیت پیشنهاد بسته سازمانی', dueAt: todayMorning, personId: peopleForAct[0]?.id, dealId: dealsForAct[0]?.id, assigneeId: owner?.id, }, { organizationId: org.id, type: 'meeting', title: 'جلسه دمو با نوا تجارت', dueAt: todayAfternoon, personId: peopleForAct[2]?.id, dealId: dealsForAct[1]?.id, assigneeId: owner?.id, }, { organizationId: org.id, type: 'task', title: 'ارسال پیشنهاد قیمت به حسین نوری', dueAt: yesterday, personId: peopleForAct[3]?.id, dealId: dealsForAct[2]?.id, assigneeId: owner?.id, }, { organizationId: org.id, type: 'task', title: 'آماده‌سازی قرارداد ارتقا آکمه', dueAt: tomorrow, personId: peopleForAct[1]?.id, dealId: dealsForAct[3]?.id, assigneeId: owner?.id, }, ], }); console.log('Seeded sample activities'); } else { console.log('Activities already seeded'); } const notesCount = await prisma.note.count({ where: { organizationId: org.id }, }); if (notesCount === 0) { const people = await prisma.person.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const deals = await prisma.deal.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const reminder = new Date(); reminder.setDate(reminder.getDate() + 2); reminder.setHours(9, 0, 0, 0); await prisma.note.createMany({ data: [ { organizationId: org.id, authorId: owner?.id, title: 'نکته خریدار آکمه', body: 'بودجه امسال تا پایان مهر باید مصرف شود. تصمیم‌گیر اصلی سارا محمدی است.', noteType: 'important', pinned: true, color: 'mint', tags: ['بودجه', 'تصمیم‌گیر'], personId: people[0]?.id, dealId: deals[0]?.id, reminderAt: reminder, }, { organizationId: org.id, authorId: owner?.id, body: 'در دمو نوا روی گزارش مسیر فروش خیلی حساس بودند. نسخه چاپی PDF را آماده کنیم.', noteType: 'call_result', color: 'sky', tags: ['دمو'], personId: people[2]?.id, dealId: deals[1]?.id, }, { organizationId: org.id, authorId: owner?.id, title: 'یادداشت آزاد', body: 'حسین نوری ترجیح می‌دهد تماس‌ها عصر باشد.', noteType: 'next_action', color: 'sand', tags: ['زمان‌بندی'], personId: people[3]?.id, }, ], }); console.log('Seeded sample notes'); } else { await prisma.note.updateMany({ where: { organizationId: org.id, title: 'نکته خریدار آکمه' }, data: { noteType: 'important' }, }); await prisma.note.updateMany({ where: { organizationId: org.id, body: { contains: 'در دمو نوا', }, }, data: { noteType: 'call_result' }, }); await prisma.note.updateMany({ where: { organizationId: org.id, title: 'یادداشت آزاد' }, data: { noteType: 'next_action' }, }); console.log('Notes already seeded'); } const customFieldCount = await prisma.customFieldDefinition.count({ where: { organizationId: org.id }, }); if (customFieldCount === 0) { await prisma.customFieldDefinition.createMany({ data: [ { organizationId: org.id, entityType: 'person', key: 'preferred_channel', label: 'کانال ترجیحی', fieldType: 'select', options: ['تلفن', 'واتساپ', 'ایمیل'], required: false, position: 0, }, { organizationId: org.id, entityType: 'company', key: 'industry', label: 'صنعت', fieldType: 'text', required: false, position: 0, }, { organizationId: org.id, entityType: 'deal', key: 'priority', label: 'اولویت', fieldType: 'select', options: ['کم', 'متوسط', 'زیاد'], required: false, position: 0, }, ], }); const person = await prisma.person.findFirst({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); if (person) { await prisma.person.update({ where: { id: person.id }, data: { customFields: { preferred_channel: 'واتساپ' } }, }); } console.log('Seeded sample custom fields'); } else { console.log('Custom fields already seeded'); } const productCount = await prisma.product.count({ where: { organizationId: org.id }, }); if (productCount === 0) { const created = await prisma.product.createMany({ data: [ { organizationId: org.id, name: `لایسنس سازمانی ${brand.nameFa}`, sku: 'MENTO-ORG', unitPrice: 80_000_000, currency: 'IRR', }, { organizationId: org.id, name: 'پشتیبانی سالانه', sku: 'MENTO-SUP', unitPrice: 20_000_000, currency: 'IRR', }, { organizationId: org.id, name: 'مشاوره پیاده‌سازی', sku: 'MENTO-IMP', unitPrice: 15_000_000, currency: 'IRR', }, ], }); void created; const products = await prisma.product.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const deal = await prisma.deal.findFirst({ where: { organizationId: org.id, title: 'فروش بسته سازمانی آکمه' }, }); if (deal && products[0] && products[1]) { await prisma.dealLineItem.createMany({ data: [ { dealId: deal.id, productId: products[0].id, name: products[0].name, quantity: 1, unitPrice: products[0].unitPrice ?? 0, currency: 'IRR', position: 0, }, { dealId: deal.id, productId: products[1].id, name: products[1].name, quantity: 1, unitPrice: products[1].unitPrice ?? 0, currency: 'IRR', position: 1, }, ], }); await prisma.deal.update({ where: { id: deal.id }, data: { amount: Number(products[0].unitPrice ?? 0) + Number(products[1].unitPrice ?? 0), }, }); } console.log('Seeded sample products'); } else { console.log('Products already seeded'); } if (owner) { const sampleDeal = await prisma.deal.findFirst({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const sampleActivity = await prisma.activity.findFirst({ where: { organizationId: org.id, assigneeId: owner.id }, orderBy: { createdAt: 'asc' }, }); const ensure = async (type: string, data: { title: string; body: string; link: string; entityType: string; entityId: string | null; readAt?: Date | null; }) => { const found = await prisma.notification.findFirst({ where: { organizationId: org.id, userId: owner.id, type }, select: { id: true }, }); if (found) return; await prisma.notification.create({ data: { organizationId: org.id, userId: owner.id, type, title: data.title, body: data.body, link: data.link, entityType: data.entityType, entityId: data.entityId, readAt: data.readAt ?? null, }, }); }; await ensure('deal_moved', { title: 'جابه‌جایی معامله در مسیر فروش', body: sampleDeal ? `«${sampleDeal.title}» به مرحله جدید منتقل شد` : 'یک معامله جابه‌جا شد', link: sampleDeal ? `/deals/${sampleDeal.id}` : '/pipeline', entityType: 'deal', entityId: sampleDeal?.id ?? null, }); await ensure('activity_assigned', { title: 'فعالیت جدید برای شما', body: sampleActivity?.title ?? 'پیگیری نمونه', link: sampleActivity ? `/activities/${sampleActivity.id}` : '/activities', entityType: 'activity', entityId: sampleActivity?.id ?? null, }); await ensure('note_reminder', { title: 'یادآوری یادداشت', body: 'یادداشت نمونه با یادآور', link: '/notes', entityType: 'note', entityId: null, readAt: new Date(), }); console.log('Ensured sample notifications'); } const leadFormCount = await prisma.leadForm.count({ where: { organizationId: org.id }, }); if (leadFormCount === 0) { const defaultPipeline = await prisma.pipeline.findFirst({ where: { organizationId: org.id, isDefault: true }, }); const sales = await prisma.user.findFirst({ where: { organizationId: org.id, email: 'sales@demo.local' }, }); await prisma.leadForm.create({ data: { organizationId: org.id, name: 'فرم تماس سایت', source: 'وب‌سایت', pipelineId: defaultPipeline?.id, thankYouMessage: 'پیام شما ثبت شد. به‌زودی با شما تماس می‌گیریم.', assigneeIds: [owner?.id, sales?.id].filter(Boolean) as string[], fields: { create: [ { key: 'firstName', label: 'نام', fieldType: 'text', required: true, mapTo: 'person.firstName', position: 0, }, { key: 'lastName', label: 'نام خانوادگی', fieldType: 'text', required: true, mapTo: 'person.lastName', position: 1, }, { key: 'mobile', label: 'موبایل', fieldType: 'phone', required: true, mapTo: 'person.mobile', position: 2, }, { key: 'email', label: 'ایمیل', fieldType: 'email', required: false, mapTo: 'person.email', position: 3, }, { key: 'message', label: 'پیام', fieldType: 'textarea', required: false, mapTo: 'deal.notes', position: 4, }, ], }, }, }); console.log('Seeded sample lead form'); } const quoteCount = await prisma.quote.count({ where: { organizationId: org.id } }); if (quoteCount === 0) { const deal = await prisma.deal.findFirst({ where: { organizationId: org.id }, include: { lineItems: { orderBy: { position: 'asc' } } }, orderBy: { createdAt: 'asc' }, }); const product = await prisma.product.findFirst({ where: { organizationId: org.id, isActive: true }, }); if (deal) { const validUntil = new Date(); validUntil.setDate(validUntil.getDate() + 14); await prisma.quote.create({ data: { organizationId: org.id, number: 'PF-0001', title: `پیش‌فاکتور — ${deal.title}`, status: 'draft', currency: deal.currency || 'IRR', notes: 'پیش‌فاکتور نمونه', validUntil, personId: deal.personId, companyId: deal.companyId, dealId: deal.id, ownerId: owner?.id ?? deal.ownerId, lineItems: { create: deal.lineItems.length > 0 ? deal.lineItems.map((item, index) => ({ productId: item.productId, name: item.name, quantity: item.quantity, unitPrice: item.unitPrice, currency: item.currency, position: index, })) : [ { productId: product?.id, name: product?.name ?? 'خدمت مشاوره', quantity: 1, unitPrice: product?.unitPrice ?? 15000000, currency: 'IRR', position: 0, }, ], }, }, }); console.log('Seeded sample quote'); } } const invoiceCount = await prisma.invoice.count({ where: { organizationId: org.id }, }); if (invoiceCount === 0) { const quote = await prisma.quote.findFirst({ where: { organizationId: org.id }, include: { lineItems: { orderBy: { position: 'asc' } } }, orderBy: { createdAt: 'asc' }, }); const deal = quote ? null : await prisma.deal.findFirst({ where: { organizationId: org.id }, include: { lineItems: { orderBy: { position: 'asc' } } }, orderBy: { createdAt: 'asc' }, }); const source = quote ?? deal; if (source) { const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 30); const lineSource = source.lineItems; const total = lineSource.reduce( (sum, item) => sum + Number(item.quantity) * Number(item.unitPrice), 0, ); const half = Math.round(total / 2); const installment1 = new Date(); installment1.setDate(installment1.getDate() + 15); const installment2 = new Date(); installment2.setDate(installment2.getDate() + 45); await prisma.invoice.create({ data: { organizationId: org.id, number: 'FA-0001', title: `فاکتور — ${source.title}`, status: 'issued', currency: source.currency || 'IRR', notes: 'فاکتور نمونه با دو قسط', issuedAt: new Date(), dueDate, personId: source.personId, companyId: source.companyId, dealId: quote ? quote.dealId : deal!.id, quoteId: quote?.id ?? null, ownerId: owner?.id ?? null, lineItems: { create: lineSource.length > 0 ? lineSource.map((item, index) => ({ productId: item.productId, name: item.name, quantity: item.quantity, unitPrice: item.unitPrice, currency: item.currency, position: index, })) : [ { name: 'خدمت مشاوره', quantity: 1, unitPrice: 15000000, currency: 'IRR', position: 0, }, ], }, installments: { create: [ { position: 0, dueDate: installment1, amount: half || 10000000, currency: 'IRR', status: 'pending', }, { position: 1, dueDate: installment2, amount: total - half || 10000000, currency: 'IRR', status: 'pending', }, ], }, }, }); console.log('Seeded sample invoice'); } } // دسترسی پرونده‌ای: بخشی از پرونده‌ها برای کارشناس فروش const salesUser = await prisma.user.findFirst({ where: { organizationId: org.id, email: salesEmail }, }); if (owner) { await prisma.person.updateMany({ where: { organizationId: org.id, ownerId: null }, data: { ownerId: owner.id }, }); await prisma.company.updateMany({ where: { organizationId: org.id, ownerId: null }, data: { ownerId: owner.id }, }); } if (salesUser && owner) { const salesDealCount = await prisma.deal.count({ where: { organizationId: org.id, ownerId: salesUser.id }, }); if (salesDealCount === 0) { const dealsForSales = await prisma.deal.findMany({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, take: 2, }); for (const deal of dealsForSales) { await prisma.deal.update({ where: { id: deal.id }, data: { ownerId: salesUser.id }, }); if (deal.personId) { await prisma.person.update({ where: { id: deal.personId }, data: { ownerId: salesUser.id }, }); } if (deal.companyId) { await prisma.company.update({ where: { id: deal.companyId }, data: { ownerId: salesUser.id }, }); } } const act = await prisma.activity.findFirst({ where: { organizationId: org.id, assigneeId: owner.id }, orderBy: { createdAt: 'asc' }, }); if (act) { await prisma.activity.update({ where: { id: act.id }, data: { assigneeId: salesUser.id }, }); } const quote = await prisma.quote.findFirst({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); if (quote) { await prisma.quote.update({ where: { id: quote.id }, data: { ownerId: salesUser.id }, }); } console.log('Assigned sample records to sales user for ACL demo'); } } const goalCount = await prisma.salesGoal.count({ where: { organizationId: org.id }, }); if (goalCount === 0 && owner) { const now = new Date(); const periodStart = new Date( Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1), ); const periodEnd = new Date( Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0, 23, 59, 59, 999), ); await prisma.salesGoal.create({ data: { organizationId: org.id, userId: null, type: 'sales', title: 'هدف فروش سازمانی ماه جاری', periodStart, periodEnd, targetAmount: 200_000_000, currency: 'IRR', notes: 'هدف نمونه برای دمو', }, }); const salesUser = await prisma.user.findFirst({ where: { organizationId: org.id, email: salesEmail }, }); if (salesUser) { await prisma.salesGoal.create({ data: { organizationId: org.id, userId: salesUser.id, type: 'sales', title: 'هدف شخصی کارشناس فروش', periodStart, periodEnd, targetAmount: 80_000_000, currency: 'IRR', }, }); } console.log('Seeded sample sales goals'); } const ticketCount = await prisma.ticket.count({ where: { organizationId: org.id }, }); if (ticketCount === 0 && owner) { const person = await prisma.person.findFirst({ where: { organizationId: org.id }, orderBy: { createdAt: 'asc' }, }); const supportOrSales = (await prisma.user.findFirst({ where: { organizationId: org.id, email: 'sales@demo.local' }, })) ?? owner; await prisma.ticket.create({ data: { organizationId: org.id, number: 'TK-0001', subject: 'درخواست راهنمایی نصب', description: 'مشتری برای راه‌اندازی اولیه کمک می‌خواهد.', status: 'open', priority: 'normal', personId: person?.id ?? null, companyId: person?.companyId ?? null, assigneeId: supportOrSales.id, reporterId: owner.id, slaDueAt: new Date(Date.now() + 24 * 60 * 60 * 1000), messages: { create: { authorId: owner.id, body: 'مشتری برای راه‌اندازی اولیه کمک می‌خواهد.', isInternal: false, }, }, }, }); console.log('Seeded sample ticket'); } else { await prisma.ticket.updateMany({ where: { organizationId: org.id, slaDueAt: null, status: { in: ['open', 'in_progress', 'waiting'] }, }, data: { slaDueAt: new Date(Date.now() + 24 * 60 * 60 * 1000), }, }); } const faqCount = await prisma.faqArticle.count({ where: { organizationId: org.id }, }); if (faqCount === 0) { await prisma.faqArticle.createMany({ data: [ { organizationId: org.id, question: 'چطور وارد پنل شوم؟', answer: 'از صفحه ورود با ایمیل سازمانی وارد شوید. در صورت فراموشی رمز، از مدیر سازمان بخواهید رمز را بازنشانی کند.', category: 'ورود', isPublished: true, position: 1, }, { organizationId: org.id, question: 'نصب اولیه چقدر طول می‌کشد؟', answer: 'راه‌اندازی اولیه معمولاً کمتر از یک ساعت است. فایل راهنما و ویدیوی کوتاه در بخش دانش موجود است.', category: 'نصب', isPublished: true, position: 2, }, { organizationId: org.id, question: 'چطور تیکت پشتیبانی ثبت کنم؟', answer: 'از منوی تیکت‌ها، تیکت جدید بسازید و موضوع و شرح را وارد کنید. قبل از ثبت، پیشنهادهای FAQ را هم ببینید.', category: 'پشتیبانی', isPublished: true, position: 3, }, ], }); console.log('Seeded sample FAQ articles'); } } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });