import { AdminRole, PrismaClient, VerificationStep } from '@prisma/client'; const prisma = new PrismaClient(); // Placeholder — replace with a real bcrypt hash once the auth module lands. const PLACEHOLDER_PASSWORD_HASH = 'placeholder-hash-not-for-production'; // Served from the user-app's own public/ folder. This is the same placeholder // image the legacy app used for missing/failed product imagery (no-items.png). // Replace these rows with real photo URLs when product photography exists. const PLACEHOLDER_PRODUCT_IMAGE = '/img/no-items.png'; async function main() { // ── Cities ──────────────────────────────────────────────────────────── const [tehran, mashhad, isfahan, tabriz] = await Promise.all([ prisma.city.upsert({ where: { id: 'city-tehran' }, update: {}, create: { id: 'city-tehran', name: 'تهران', province: 'تهران' } }), prisma.city.upsert({ where: { id: 'city-mashhad' }, update: {}, create: { id: 'city-mashhad', name: 'مشهد', province: 'خراسان رضوی' } }), prisma.city.upsert({ where: { id: 'city-isfahan' }, update: {}, create: { id: 'city-isfahan', name: 'اصفهان', province: 'اصفهان' } }), prisma.city.upsert({ where: { id: 'city-tabriz' }, update: {}, create: { id: 'city-tabriz', name: 'تبریز', province: 'آذربایجان شرقی' } }), ]); // ── Branches & workshop ────────────────────────────────────────────── const branchTehran = await prisma.branch.upsert({ where: { id: 'branch-tehran-bazaar' }, update: {}, create: { id: 'branch-tehran-bazaar', name: 'شعبه بازار بزرگ تهران', cityId: tehran.id }, }); const branchMashhad = await prisma.branch.upsert({ where: { id: 'branch-mashhad-main' }, update: {}, create: { id: 'branch-mashhad-main', name: 'شعبه مرکزی مشهد', cityId: mashhad.id }, }); await prisma.workshop.upsert({ where: { id: 'workshop-central' }, update: {}, create: { id: 'workshop-central', name: 'کارگاه مرکزی ساخت طلا' }, }); // ── Admin users (sales agents + one super admin) ──────────────────── await prisma.adminUser.upsert({ where: { mobile: '09120000001' }, update: {}, create: { mobile: '09120000001', fullName: 'مدیر ارشد سیستم', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SUPER_ADMIN, branchId: branchTehran.id, }, }); const agentReza = await prisma.adminUser.upsert({ where: { mobile: '09121234567' }, update: {}, create: { mobile: '09121234567', fullName: 'رضا احمدی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SALES_EXPERT, branchId: branchTehran.id, wagePercent: 1.5, }, }); const agentSara = await prisma.adminUser.upsert({ where: { mobile: '09129876543' }, update: {}, create: { mobile: '09129876543', fullName: 'سارا کریمی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SALES_EXPERT, branchId: branchMashhad.id, wagePercent: 1.2, }, }); await prisma.adminUser.upsert({ where: { mobile: '09123334444' }, update: {}, create: { mobile: '09123334444', fullName: 'هومن یزدانی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.WORKSHOP_SUPERVISOR, }, }); await prisma.adminUser.upsert({ where: { mobile: '09125556666' }, update: {}, create: { mobile: '09125556666', fullName: 'فرزانه محمودی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.ACCOUNTANT, branchId: branchTehran.id, }, }); // ── Customers ──────────────────────────────────────────────────────── const customerData = [ { id: 'customer-1', fullName: 'علی محمدی', mobile: '09151112233', nationalCode: '0012345678', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '200', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-2', fullName: 'مریم حسینی', mobile: '09151112234', nationalCode: '0012345679', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '100', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-3', fullName: 'جواد نوروزی', mobile: '09151112235', nationalCode: '0012345680', cityId: mashhad.id, referredByAgentId: agentSara.id, goldCreditLimitGrams: '50', verificationStep: VerificationStep.STEP_TWO_SUBMITTED, }, { id: 'customer-4', fullName: 'نرگس ابراهیمی', mobile: '09151112236', nationalCode: '0012345681', cityId: isfahan.id, referredByAgentId: null, goldCreditLimitGrams: '30', verificationStep: VerificationStep.UNVERIFIED, }, { id: 'customer-5', fullName: 'کامران صادقی', mobile: '09151112237', nationalCode: '0012345682', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '500', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-6', fullName: 'زهرا قاسمی', mobile: '09151112238', nationalCode: '0012345683', cityId: tabriz.id, referredByAgentId: agentSara.id, goldCreditLimitGrams: '80', verificationStep: VerificationStep.STEP_ONE_SUBMITTED, }, ]; const customers = []; for (const c of customerData) { customers.push( await prisma.customer.upsert({ where: { id: c.id }, update: {}, create: { id: c.id, fullName: c.fullName, mobile: c.mobile, nationalCode: c.nationalCode, passwordHash: PLACEHOLDER_PASSWORD_HASH, cityId: c.cityId, referredByAgentId: c.referredByAgentId, goldCreditLimitGrams: c.goldCreditLimitGrams, verificationStep: c.verificationStep, }, }), ); } const [customer1, customer2, , , customer5] = customers; // ── Catalog: categories, brands, products, variants ──────────────── const [ringsCategory, braceletsCategory, necklacesCategory, earringsCategory, coinsCategory] = await Promise.all([ prisma.category.upsert({ where: { id: 'cat-rings' }, update: {}, create: { id: 'cat-rings', title: 'انگشتر' } }), prisma.category.upsert({ where: { id: 'cat-bracelets' }, update: {}, create: { id: 'cat-bracelets', title: 'دستبند' } }), prisma.category.upsert({ where: { id: 'cat-necklaces' }, update: {}, create: { id: 'cat-necklaces', title: 'گردنبند' } }), prisma.category.upsert({ where: { id: 'cat-earrings' }, update: {}, create: { id: 'cat-earrings', title: 'گوشواره' } }), prisma.category.upsert({ where: { id: 'cat-coins' }, update: {}, create: { id: 'cat-coins', title: 'سکه و شمش' } }), ]); const brandHenza = await prisma.brand.upsert({ where: { id: 'brand-henza' }, update: {}, create: { id: 'brand-henza', title: 'هنزا طلا' }, }); const brandArtisan = await prisma.brand.upsert({ where: { id: 'brand-artisan' }, update: {}, create: { id: 'brand-artisan', title: 'مجموعه صنعتگر' }, }); const productDefs = [ { id: 'product-classic-ring', title: 'انگشتر تک نگین کلاسیک', description: 'انگشتر طلای ۱۸ عیار با طراحی ساده و مینیمال.', categoryId: ringsCategory.id, brandId: brandHenza.id, variants: [ { id: 'variant-ring-size-6', title: 'سایز ۶', sku: 'RING-CLASSIC-6', rashWeightGrams: '2.850', defaultWagePercent: '9.0' }, { id: 'variant-ring-size-7', title: 'سایز ۷', sku: 'RING-CLASSIC-7', rashWeightGrams: '3.100', defaultWagePercent: '9.0' }, ], }, { id: 'product-woven-bracelet', title: 'دستبند بافت زنجیری', description: 'دستبند بافت‌دار دست‌ساز از طلای ۱۸ عیار.', categoryId: braceletsCategory.id, brandId: brandArtisan.id, variants: [ { id: 'variant-bracelet-standard', title: 'استاندارد ۱۸ سانتی', sku: 'BRC-WOVEN-18', rashWeightGrams: '14.200', defaultWagePercent: '11.5' }, ], }, { id: 'product-pendant-necklace', title: 'گردنبند آویز اشکی', description: 'گردنبند ظریف با آویز اشکی و زنجیر نازک.', categoryId: necklacesCategory.id, brandId: brandHenza.id, variants: [ { id: 'variant-necklace-40cm', title: 'زنجیر ۴۰ سانتی', sku: 'NCK-TEARDROP-40', rashWeightGrams: '5.600', defaultWagePercent: '10.0' }, { id: 'variant-necklace-45cm', title: 'زنجیر ۴۵ سانتی', sku: 'NCK-TEARDROP-45', rashWeightGrams: '6.100', defaultWagePercent: '10.0' }, ], }, { id: 'product-hoop-earrings', title: 'گوشواره حلقه‌ای کلاسیک', description: 'گوشواره حلقه‌ای طلای ۱۸ عیار مناسب استفاده روزمره.', categoryId: earringsCategory.id, brandId: brandArtisan.id, variants: [ { id: 'variant-hoop-small', title: 'کوچک', sku: 'ERG-HOOP-S', rashWeightGrams: '1.900', defaultWagePercent: '8.5' }, { id: 'variant-hoop-large', title: 'بزرگ', sku: 'ERG-HOOP-L', rashWeightGrams: '3.400', defaultWagePercent: '8.5' }, ], }, { id: 'product-full-coin', title: 'سکه تمام بهار آزادی', description: 'سکه طلای تمام بهار آزادی، عیار استاندارد.', categoryId: coinsCategory.id, brandId: brandHenza.id, variants: [ { id: 'variant-coin-full', title: 'تمام سکه', sku: 'COIN-FULL', rashWeightGrams: '8.133', defaultWagePercent: '2.0' }, { id: 'variant-coin-half', title: 'نیم سکه', sku: 'COIN-HALF', rashWeightGrams: '4.066', defaultWagePercent: '2.5' }, ], }, ]; const createdVariantIds: Record = {}; let imageCount = 0; for (const p of productDefs) { const product = await prisma.product.upsert({ where: { id: p.id }, update: {}, create: { id: p.id, title: p.title, description: p.description, categoryId: p.categoryId, brandId: p.brandId, }, }); for (const v of p.variants) { await prisma.productVariant.upsert({ where: { id: v.id }, update: {}, create: { id: v.id, productId: product.id, title: v.title, sku: v.sku, rashWeightGrams: v.rashWeightGrams, defaultWagePercent: v.defaultWagePercent, }, }); createdVariantIds[v.id] = v.id; } // Product photography does not exist in this project: legacy served it from // the old Laravel host (Product.thumb.full), so there is nothing to migrate. // Seed the placeholder the legacy app itself used for missing imagery so // every product card renders an image instead of collapsing to nothing. await prisma.productImage.upsert({ where: { id: `${p.id}-img-1` }, update: { url: PLACEHOLDER_PRODUCT_IMAGE }, create: { id: `${p.id}-img-1`, productId: product.id, url: PLACEHOLDER_PRODUCT_IMAGE, sortOrder: 0, }, }); imageCount += 1; } // ── Featured collection ────────────────────────────────────────────── const featured = await prisma.collection.upsert({ where: { id: 'collection-featured' }, update: {}, create: { id: 'collection-featured', title: 'پرفروش‌های این هفته', description: 'منتخبی از محبوب‌ترین محصولات فروشگاه.' }, }); await prisma.collectionProduct.upsert({ where: { collectionId_productId: { collectionId: featured.id, productId: 'product-classic-ring' } }, update: {}, create: { collectionId: featured.id, productId: 'product-classic-ring' }, }); await prisma.collectionProduct.upsert({ where: { collectionId_productId: { collectionId: featured.id, productId: 'product-pendant-necklace' } }, update: {}, create: { collectionId: featured.id, productId: 'product-pendant-necklace' }, }); await prisma.collectionProduct.upsert({ where: { collectionId_productId: { collectionId: featured.id, productId: 'product-full-coin' } }, update: {}, create: { collectionId: featured.id, productId: 'product-full-coin' }, }); // ── Gold price quote (illustrative, not live-market-accurate) ─────── await prisma.goldPriceQuote.create({ data: { karatPerMille: 750, pricePerGramRial: '61500000', source: 'seed-illustrative' }, }); // ── A delivered order + posted debt, so the dashboard shows real numbers ── const existingDemoOrder = await prisma.order.findUnique({ where: { orderNumber: 'ORD-SEED-0001' } }); if (!existingDemoOrder) { const rashWeight = '3.100'; // variant-ring-size-7 const wagePercent = 9.0; const discountPercent = 0; const computedWeight = (parseFloat(rashWeight) * (1 + (wagePercent - discountPercent) / 100)).toFixed(4); const quantity = 1; const flatRialChargePerUnit = '500000'; // e.g. a small stone-setting fee const order = await prisma.order.create({ data: { orderNumber: 'ORD-SEED-0001', customerId: customer1.id, branchId: branchTehran.id, status: 'DELIVERED_TO_CUSTOMER', totalGoldWeightGrams: computedWeight, wageWeightGrams: (parseFloat(computedWeight) - parseFloat(rashWeight)).toFixed(4), flatRialCharge: flatRialChargePerUnit, deliveredAt: new Date(), items: { create: [ { productVariantId: 'variant-ring-size-7', quantity, rashWeightGramsSnapshot: rashWeight, wagePercentSnapshot: wagePercent.toString(), discountPercentSnapshot: discountPercent.toString(), computedWeightGrams: computedWeight, flatRialCharge: flatRialChargePerUnit, }, ], }, statusEvents: { create: [{ toStatus: 'PLACED' }, { toStatus: 'ADMIN_APPROVED' }, { toStatus: 'DELIVERED_TO_CUSTOMER' }], }, }, }); await prisma.ledgerEntry.create({ data: { customerId: customer1.id, currency: 'GOLD_GRAM', direction: 'DEBIT', sourceType: 'ORDER_STOCK', amount: computedWeight, orderId: order.id, confirmed: true, confirmedAt: new Date(), note: 'بدهی وزنی طلا هنگام تحویل (داده آزمایشی)', }, }); await prisma.ledgerEntry.create({ data: { customerId: customer1.id, currency: 'RIAL', direction: 'DEBIT', sourceType: 'ORDER_STOCK', amount: flatRialChargePerUnit, orderId: order.id, confirmed: true, confirmedAt: new Date(), note: 'هزینه ثابت ریالی هنگام تحویل (داده آزمایشی)', }, }); await prisma.customer.update({ where: { id: customer1.id }, data: { goldDebtGrams: { increment: computedWeight }, fiatDebtRial: { increment: flatRialChargePerUnit } }, }); // A pending repayment awaiting admin confirmation, to demo the confirm flow. await prisma.ledgerEntry.create({ data: { customerId: customer1.id, currency: 'GOLD_GRAM', direction: 'CREDIT', sourceType: 'REPAYMENT_MELTED_GOLD', amount: '1.5000', karatPerMille: 750, confirmed: false, note: 'تسویه با طلای آب‌شده، در انتظار تایید (داده آزمایشی)', }, }); } // customer2 and customer5 get existing debt/credit variety for the dashboard. await prisma.customer.update({ where: { id: customer2.id }, data: { goldDebtGrams: '8.2500', fiatDebtRial: '2000000' } }); await prisma.customer.update({ where: { id: customer5.id }, data: { goldDebtGrams: '0', fiatDebtRial: '0' } }); console.log('Seed complete:', { cities: 4, branches: 2, adminUsers: 5, customers: customers.length, products: productDefs.length, variants: Object.keys(createdVariantIds).length, productImages: imageCount, }); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });