Files
tehran_taj/apps/api/prisma/seed.ts
T
2026-09-09 06:46:27 +03:30

442 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string> = {};
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();
});