import { AdminRole, Customer, CustomerKind, OrderChannel, PrismaClient, VerificationStep, } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { ALL_RIDS } from '../src/modules/auth/acl.constants'; const prisma = new PrismaClient(); // Real bcrypt hash of DEV_PASSWORD, so seeded accounts can actually log in. // Development credentials only — never seed these into a real deployment. const DEV_PASSWORD = 'admin1234'; const PLACEHOLDER_PASSWORD_HASH = bcrypt.hashSync(DEV_PASSWORD, 10); // A single number authorized to sign in to BOTH apps for local development, // with its password (admin panel) equal to the number itself. DEV-ONLY: this // file is never run against a real deployment — only by hand via // `pnpm prisma:seed` / `npx prisma db seed` against a local database. const DEV_TEST_PHONE = '09128081392'; const DEV_TEST_PHONE_PASSWORD_HASH = bcrypt.hashSync(DEV_TEST_PHONE, 10); // 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'; /** * Role definitions and their permissions — the one part of this file a real * deployment needs (the admin screens and the legacy import map onto these * ids). Idempotent: upserts and skipDuplicates only. */ async function seedAccessControl() { // ── Admin user types + permissions (legacy `permission(rid, ut)`) ──── // // In legacy these lived in the dynamic `config_static` lookup under the key // `__list__ut` and were created from the UI (views/user/Permission.vue), so // there is no legacy seeder to copy. Id 85 is seeded because Permission.vue // hardcodes it as the default selection (`setFieldsValue({ut:"85"})`). // // Only leaf rids are stored; ancestors are derived at runtime by // expandWithParents() / checkParent(), exactly as legacy did. const userTypes: { id: number; title: string; rids: readonly string[] }[] = [ // Full access — every rid the routers gate. { id: 85, title: 'مدیر ارشد', rids: ALL_RIDS }, { id: 86, title: 'کارشناس فروش', rids: [ 'order.list', 'order.create', 'order.omde', 'order.makeOrder', 'persons.list.client.omde', 'persons.list.client.omde.view', 'persons.list.client.single', 'persons.list.client.single.view', 'persons.create', 'product.list', 'dashboard.view', 'comment', ], }, { id: 87, title: 'حسابدار', rids: [ 'document.list', 'document.create', 'document.list.edit', 'invoice.list', 'invoice.create', 'wallet', 'config.wallet', 'report.order.item', 'report.product', 'config.report', 'dashboard.view', ], }, { id: 88, title: 'سرپرست کارگاه', rids: ['workshop.list', 'workshop.create', 'order.tamin', 'order.list', 'product.list'], }, { id: 89, title: 'کاربر شعبه', rids: ['buy.order.list', 'order.create', 'persons.create', 'branch.list'], }, ]; for (const ut of userTypes) { await prisma.userType.upsert({ where: { id: ut.id }, update: { title: ut.title }, create: { id: ut.id, title: ut.title }, }); await prisma.permission.createMany({ data: ut.rids.map((rid) => ({ rid, userTypeId: ut.id })), skipDuplicates: true, }); } } /** * Everything below is DEMO data: sample cities, branches, accounts with known * passwords (one super admin whose password is its own phone number), sample * customers, products, orders and posted debt. The app runs this seed once on * a fresh database at boot (src/prisma/bootstrap.ts), so without this gate a * production first boot would mix fake customers, fake debt and open admin * accounts into the real data (SEC-05). Production never gets it unless * SEED_DEMO_DATA=true is set on purpose. */ function demoDataAllowed(): boolean { if (process.env.SEED_DEMO_DATA?.trim() === 'true') return true; return process.env.NODE_ENV !== 'production'; } async function main() { await seedAccessControl(); if (!demoDataAllowed()) { console.log('seed: production — roles and permissions only; demo data skipped (set SEED_DEMO_DATA=true to force)'); return; } // ── Cities ──────────────────────────────────────────────────────────── // A tree, as legacy's: country > province > city. The country is a heading // in the admin pickers; provinces and cities are selectable. const city = (id: string, name: string, parentId: string | null) => prisma.city.upsert({ where: { id }, update: {}, create: { id, name, parentId } }); await city('city-iran', 'ایران', null); await Promise.all([ city('city-prov-tehran', 'استان تهران', 'city-iran'), city('city-prov-khorasan-razavi', 'استان خراسان رضوی', 'city-iran'), city('city-prov-isfahan', 'استان اصفهان', 'city-iran'), city('city-prov-east-azarbaijan', 'استان آذربایجان شرقی', 'city-iran'), ]); const [tehran, mashhad, isfahan, tabriz] = await Promise.all([ city('city-tehran', 'تهران', 'city-prov-tehran'), city('city-mashhad', 'مشهد', 'city-prov-khorasan-razavi'), city('city-isfahan', 'اصفهان', 'city-prov-isfahan'), city('city-tabriz', 'تبریز', 'city-prov-east-azarbaijan'), ]); // ── 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) ──────────────────── // DEV-ONLY test login, per explicit request: authorized in BOTH the admin // panel and the user-app, password = the phone number itself. Super admin // so it can reach every screen while testing. Do not carry this into a real // deployment's seed data. await prisma.adminUser.upsert({ where: { mobile: DEV_TEST_PHONE }, update: { passwordHash: DEV_TEST_PHONE_PASSWORD_HASH, userTypeId: 85, branchId: null }, create: { mobile: DEV_TEST_PHONE, fullName: 'کاربر تست توسعه', passwordHash: DEV_TEST_PHONE_PASSWORD_HASH, role: AdminRole.SUPER_ADMIN, userTypeId: 85, branchId: null, }, }); await prisma.adminUser.upsert({ where: { mobile: '09120000001' }, update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 85, branchId: null }, create: { mobile: '09120000001', fullName: 'مدیر ارشد سیستم', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SUPER_ADMIN, userTypeId: 85, // Head office: deliberately branch-less. Legacy scoped every list by // branch_id whenever the user had one, so head-office staff must have none // or they would see only a single branch's data. branchId: null, }, }); const agentReza = await prisma.adminUser.upsert({ where: { mobile: '09121234567' }, update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 86 }, create: { mobile: '09121234567', fullName: 'رضا احمدی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SALES_EXPERT, userTypeId: 86, branchId: branchTehran.id, wagePercent: 1.5, }, }); const agentSara = await prisma.adminUser.upsert({ where: { mobile: '09129876543' }, update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 86 }, create: { mobile: '09129876543', fullName: 'سارا کریمی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.SALES_EXPERT, userTypeId: 86, branchId: branchMashhad.id, wagePercent: 1.2, }, }); await prisma.adminUser.upsert({ where: { mobile: '09123334444' }, update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 88 }, create: { mobile: '09123334444', fullName: 'هومن یزدانی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.WORKSHOP_SUPERVISOR, userTypeId: 88, }, }); await prisma.adminUser.upsert({ where: { mobile: '09125556666' }, update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 87, branchId: null }, create: { mobile: '09125556666', fullName: 'فرزانه محمودی', passwordHash: PLACEHOLDER_PASSWORD_HASH, role: AdminRole.ACCOUNTANT, userTypeId: 87, branchId: null, }, }); // ── Customers ──────────────────────────────────────────────────────── const customerData = [ { // DEV-ONLY test login, per explicit request: same number as the admin // panel account above, so it can also sign in to the user-app (OTP step // matches on mobile against a real seeded customer — see LoginView.vue). // Fully verified so no onboarding step blocks it. Not for a real deployment. id: 'customer-dev-test', kind: CustomerKind.APP_SHOPKEEPER, memberCode: 'DEV-TEST', fullName: 'کاربر تست توسعه', mobile: DEV_TEST_PHONE, nationalCode: '0000000000', cityId: tehran.id, referredByAgentId: null, goldCreditLimitGrams: '1000', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-1', kind: CustomerKind.BANKER, storeName: 'طلای محمدی', pasazhName: 'پاساژ بزرگ', zone: 'بازار', memberCode: 'M-1001', phone: '02133445566', address: 'تهران، بازار بزرگ، پلاک ۱۲', fullName: 'علی محمدی', mobile: '09151112233', nationalCode: '0012345678', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '200', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-2', kind: CustomerKind.APP_SHOPKEEPER, storeName: 'گالری حسینی', zone: 'ونک', memberCode: 'M-1002', phone: '02188776655', fullName: 'مریم حسینی', mobile: '09151112234', nationalCode: '0012345679', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '100', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-3', kind: CustomerKind.WALK_IN_SHOPKEEPER, storeName: 'زرگری نوروزی', pasazhName: 'پاساژ رضا', zone: 'طرق', memberCode: 'M-1003', fullName: 'جواد نوروزی', mobile: '09151112235', nationalCode: '0012345680', cityId: mashhad.id, referredByAgentId: agentSara.id, goldCreditLimitGrams: '50', verificationStep: VerificationStep.STEP_TWO_SUBMITTED, }, { id: 'customer-4', kind: CustomerKind.APP_SHOPKEEPER, memberCode: 'M-1004', fullName: 'نرگس ابراهیمی', mobile: '09151112236', nationalCode: '0012345681', cityId: isfahan.id, referredByAgentId: null, goldCreditLimitGrams: '30', verificationStep: VerificationStep.UNVERIFIED, // archived, so the "لیست بایگانی" grid has a row and the type lists // correctly exclude it isArchived: true, }, { id: 'customer-5', kind: CustomerKind.BANKER, storeName: 'صادقی و پسران', zone: 'بازار', memberCode: 'M-1005', phone: '02155443322', fullName: 'کامران صادقی', mobile: '09151112237', nationalCode: '0012345682', cityId: tehran.id, referredByAgentId: agentReza.id, goldCreditLimitGrams: '500', verificationStep: VerificationStep.VERIFIED, }, { id: 'customer-6', kind: CustomerKind.WALK_IN_SHOPKEEPER, storeName: 'طلاسرای قاسمی', zone: 'ائل گلی', memberCode: 'M-1006', fullName: 'زهرا قاسمی', mobile: '09151112238', nationalCode: '0012345683', cityId: tabriz.id, referredByAgentId: agentSara.id, goldCreditLimitGrams: '80', verificationStep: VerificationStep.STEP_ONE_SUBMITTED, }, ]; const customers: Customer[] = []; for (const c of customerData) { customers.push( await prisma.customer.upsert({ where: { id: c.id }, // Re-assert the DESCRIPTIVE fields on an existing row so a re-seed // backfills columns added after that row was first created. Balances, // verification state and credit limits are deliberately NOT touched — // those are live data a re-seed must never overwrite. update: { kind: c.kind, phone: 'phone' in c ? c.phone : null, address: 'address' in c ? c.address : null, storeName: 'storeName' in c ? c.storeName : null, pasazhName: 'pasazhName' in c ? c.pasazhName : null, zone: 'zone' in c ? c.zone : null, memberCode: c.memberCode, isArchived: 'isArchived' in c ? c.isArchived : false, }, 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, kind: c.kind, phone: 'phone' in c ? c.phone : null, address: 'address' in c ? c.address : null, storeName: 'storeName' in c ? c.storeName : null, pasazhName: 'pasazhName' in c ? c.pasazhName : null, zone: 'zone' in c ? c.zone : null, memberCode: c.memberCode, isArchived: 'isArchived' in c ? c.isArchived : false, }, }), ); } // Looked up by id rather than array position — a positional destructure here // silently breaks (wrong customer, no error) the moment a row is added or // reordered above, as happened when the dev-test login row was inserted first. const byId = (id: string) => customers.find((c) => c.id === id)!; const customer1 = byId('customer-1'); const customer2 = byId('customer-2'); const customer5 = byId('customer-5'); // ── 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 brandMontzar = await prisma.brand.upsert({ where: { id: 'brand-henza' }, update: { title: 'منتزر طلا' }, 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: brandMontzar.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: brandMontzar.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: brandMontzar.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 RETAIL order, so the single-sale grid (legacy `POST single-order`) has a // row of its own. Legacy kept the two channels in separate tables; here they // are separated by Order.channel. const existingRetail = await prisma.order.findUnique({ where: { orderNumber: 'ORD-SEED-0002' }, }); if (!existingRetail) { const rash = '2.400'; const wage = 7.5; const computed = (parseFloat(rash) * (1 + wage / 100)).toFixed(4); await prisma.order.create({ data: { orderNumber: 'ORD-SEED-0002', customerId: customer2.id, branchId: branchTehran.id, channel: OrderChannel.RETAIL, status: 'PLACED', totalGoldWeightGrams: computed, wageWeightGrams: (parseFloat(computed) - parseFloat(rash)).toFixed(4), flatRialCharge: '0', items: { create: [ { productVariantId: 'variant-ring-size-7', quantity: 1, rashWeightGramsSnapshot: rash, wagePercentSnapshot: wage.toString(), discountPercentSnapshot: '0', computedWeightGrams: computed, flatRialCharge: '0', }, ], }, statusEvents: { create: [{ toStatus: 'PLACED' }] }, }, }); } // Order.channel was added after these demo rows first existed, so a re-seed // corrects the channel on the two named seed orders. Scoped to those exact // order numbers — it must never rewrite real orders. await prisma.order.updateMany({ where: { orderNumber: 'ORD-SEED-0001' }, data: { channel: OrderChannel.WHOLESALE }, }); await prisma.order.updateMany({ where: { orderNumber: 'ORD-SEED-0002' }, data: { channel: OrderChannel.RETAIL }, }); // ── 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, // customer1 is a بنکدار, so this belongs to the wholesale grid channel: OrderChannel.WHOLESALE, 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' } }); // ── Pre-orders (legacy "سفارش های ساختنی" / custom orders) ────────── // Design requests an admin prices and later converts into a real Order. // Seeded so the MakeOrder grid has rows to render. const preOrderSeeds = [ { id: 'preorder-0001', customerId: customer2.id, cityId: tehran.id, status: 'PLACED' as const, colorName: 'رزگلد', sizeSpec: '۱۶', stoneColor: 'سفید', minaColor: 'آبی', description: 'انگشتر طرح سنتی با نگین سفید', estimatedWeightGrams: '4.5000', }, { id: 'preorder-0002', customerId: customer5.id, cityId: mashhad.id, status: 'ADMIN_APPROVED' as const, colorName: 'زرد', sizeSpec: '۱۸', stoneColor: 'قرمز', minaColor: 'سبز', description: 'دستبند مینا کاری', estimatedWeightGrams: '9.2500', }, ]; for (const po of preOrderSeeds) { await prisma.preOrder.upsert({ where: { id: po.id }, update: {}, create: po, }); } // ── Product comments (legacy `comment` table) ─────────────────────── // Seeded across all three moderation states so the Comments grid and its // status filter have something to show. const allProducts = await prisma.product.findMany({ take: 3 }); if (allProducts.length > 0) { const commentSeeds = [ { id: 'comment-0001', productId: allProducts[0].id, customerId: customer2.id, body: 'کیفیت ساخت عالی بود، ممنون از ارسال سریع.', status: 'APPROVED' as const, }, { id: 'comment-0002', productId: allProducts[0].id, customerId: customer5.id, body: 'آیا این مدل در سایز بزرگ‌تر هم موجود می‌شود؟', status: 'PENDING' as const, }, { id: 'comment-0003', productId: allProducts[Math.min(1, allProducts.length - 1)].id, customerId: customer2.id, body: 'وزن با آنچه نوشته شده کمی تفاوت داشت.', status: 'REJECTED' as const, }, ]; for (const c of commentSeeds) { await prisma.comment.upsert({ where: { id: c.id }, update: {}, create: c }); } } // ── Variant stock + product codes (columns added in Batch 6) ──────── // Without stock every variant reads as out-of-stock, which the cart guard // rejects, so the order builder would have nothing to add. const allVariants = await prisma.productVariant.findMany(); for (const [i, v] of allVariants.entries()) { await prisma.productVariant.update({ where: { id: v.id }, data: { qty: 5 + (i % 4), isPreOrder: i % 5 === 0 }, }); } const codedProducts = await prisma.product.findMany({ orderBy: { title: 'asc' } }); for (const [i, p] of codedProducts.entries()) { await prisma.product.update({ where: { id: p.id }, data: { productCode: `P-${String(i + 1).padStart(4, '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(); });