Files
tehran_tala/apps/api/prisma/seed.ts
T

442 lines
17 KiB
TypeScript
Raw Normal View History

2026-09-09 06:46:27 +03:30
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();
});