442 lines
17 KiB
TypeScript
442 lines
17 KiB
TypeScript
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();
|
||
});
|