Compare commits

...

4 Commits

Author SHA1 Message Date
parsa aghaei
29f5122f14 chore(graphify): update knowledge graph
Some checks failed
Deploy Canina / deploy (push) Successful in 2m23s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
2026-09-05 15:16:28 +03:30
parsa aghaei
2c377cab88 fix(app): improve wallet history display, fix order details modal layout and lint errors 2026-09-05 15:16:20 +03:30
parsa aghaei
56e8029fff feat(admin): update order management and SMS rules settings UI with lint fixes 2026-09-05 15:16:12 +03:30
parsa aghaei
5e3e3958c5 feat(backend): add wallet refund transactions and enhance SMS automation rules 2026-09-05 15:16:04 +03:30
32 changed files with 158475 additions and 9718 deletions

View File

@ -402,6 +402,9 @@ model Order {
paymentMethod String? @default("card") @map("payment_method") @db.VarChar(30) paymentMethod String? @default("card") @map("payment_method") @db.VarChar(30)
shippingAddress String? @map("shipping_address") @db.Text shippingAddress String? @map("shipping_address") @db.Text
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100) trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
refundStatus String? @default("none") @map("refund_status") @db.VarChar(30) // none, wallet, gateway, partial
refundedAmount Decimal @default(0.00) @map("refunded_amount") @db.Decimal(15, 2)
refundMethod String? @map("refund_method") @db.VarChar(30) // wallet, zibal
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz() createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)

View File

@ -204,7 +204,8 @@ export class AdminController {
@Body('trackingCode') trackingCode?: string, @Body('trackingCode') trackingCode?: string,
@Body('trackingNumber') trackingNumber?: string, @Body('trackingNumber') trackingNumber?: string,
) { ) {
const finalTracking = trackingNumber !== undefined ? trackingNumber : trackingCode; const finalTracking =
trackingNumber !== undefined ? trackingNumber : trackingCode;
const order = await this.adminService.updateOrderStatus( const order = await this.adminService.updateOrderStatus(
id, id,
status, status,

View File

@ -859,22 +859,31 @@ export class AdminService {
} }
if (!shipping.province || !shipping.address) { if (!shipping.province || !shipping.address) {
let text = trimmed; let text = trimmed;
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/); const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) { if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim(); if (!shipping.fullName)
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim(); shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim(); text = text.replace(recipientMatch[0], '').trim();
} }
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/); const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) { if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim(); if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim(); text = text.replace(postalMatch[0], '').trim();
} }
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean); const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) { if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0]; if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1]; if (!shipping.city) shipping.city = parts[1];
if (!shipping.address) shipping.address = parts.slice(2).join('، '); if (!shipping.address)
shipping.address = parts.slice(2).join('، ');
} else if (parts.length === 2) { } else if (parts.length === 2) {
if (!shipping.province) shipping.province = parts[0]; if (!shipping.province) shipping.province = parts[0];
if (!shipping.address) shipping.address = parts[1]; if (!shipping.address) shipping.address = parts[1];
@ -897,25 +906,35 @@ export class AdminService {
customerFullName customerFullName
).trim(); ).trim();
const finalTracking = trackingNumber || updatedOrder.trackingNumber || ''; const finalTracking =
trackingNumber || updatedOrder.trackingNumber || '';
const orderData = { const orderData = {
orderNumber: updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8), orderNumber:
updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8),
amount: Number(updatedOrder.totalAmount || 0).toLocaleString('fa-IR'), amount: Number(updatedOrder.totalAmount || 0).toLocaleString('fa-IR'),
customerName: customerFullName, customerName: customerFullName,
customerPhone: user?.mobile || shipping.phone || '', customerPhone: user?.mobile || shipping.phone || '',
recipientName: recipientFullName, recipientName: recipientFullName,
recipientPhone: shipping.phone || user?.mobile || '', recipientPhone: shipping.phone || user?.mobile || '',
provinceCity: (shipping.province && shipping.city ? `${shipping.province} - ${shipping.city}` : shipping.province || shipping.city || 'ثبت نشده').trim(), provinceCity: (shipping.province && shipping.city
? `${shipping.province} - ${shipping.city}`
: shipping.province || shipping.city || 'ثبت نشده'
).trim(),
postalCode: shipping.postalCode || shipping.zipCode || '', postalCode: shipping.postalCode || shipping.zipCode || '',
address: (shipping.address || (typeof rawShipping === 'string' ? rawShipping : '')).trim(), address: (
shipping.address ||
(typeof rawShipping === 'string' ? rawShipping : '')
).trim(),
trackingCode: finalTracking || 'ارسال با پیک شهری', trackingCode: finalTracking || 'ارسال با پیک شهری',
shippingTrackingCode: finalTracking || 'ارسال با پیک شهری', shippingTrackingCode: finalTracking || 'ارسال با پیک شهری',
shippingMethod: finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری', shippingMethod: finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری',
orderDate: new Date().toLocaleDateString('fa-IR'), orderDate: new Date().toLocaleDateString('fa-IR'),
}; };
this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {}); this.smsService
.triggerEvent('ORDER_SHIPPED', orderData)
.catch(() => {});
} catch (smsErr) { } catch (smsErr) {
// Silently catch SMS errors to avoid breaking status update // Silently catch SMS errors to avoid breaking status update
} }
@ -944,6 +963,12 @@ export class AdminService {
); );
} }
if (order.refundStatus === 'wallet' || order.refundStatus === 'gateway') {
throw new BadRequestException(
'برای این سفارش قبلاً استرداد وجه انجام شده است',
);
}
const refundAmount = const refundAmount =
customAmount !== undefined && customAmount > 0 customAmount !== undefined && customAmount > 0
? customAmount ? customAmount
@ -980,6 +1005,9 @@ export class AdminService {
where: { id: order.id }, where: { id: order.id },
data: { data: {
status: 'cancelled', status: 'cancelled',
refundStatus: 'wallet',
refundedAmount: refundAmount,
refundMethod: 'wallet',
}, },
}); });
}); });
@ -1000,11 +1028,15 @@ export class AdminService {
customerPhone: order.user.mobile, customerPhone: order.user.mobile,
amount: Number(refundAmount || 0).toLocaleString('fa-IR'), amount: Number(refundAmount || 0).toLocaleString('fa-IR'),
balance: Number(user?.walletBalance || 0).toLocaleString('fa-IR'), balance: Number(user?.walletBalance || 0).toLocaleString('fa-IR'),
reason: reason || `استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`, reason:
reason ||
`استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`,
date: new Date().toLocaleDateString('fa-IR'), date: new Date().toLocaleDateString('fa-IR'),
}; };
this.smsService.triggerEvent('WALLET_CHARGED', refundData).catch(() => {}); this.smsService
.triggerEvent('WALLET_CHARGED', refundData)
.catch(() => {});
} catch (err) { } catch (err) {
// Silently catch SMS errors // Silently catch SMS errors
} }
@ -1290,8 +1322,14 @@ export class AdminService {
customerName: customerFullName, customerName: customerFullName,
customerPhone: updatedUser.mobile, customerPhone: updatedUser.mobile,
amount: Number(amount || 0).toLocaleString('fa-IR'), amount: Number(amount || 0).toLocaleString('fa-IR'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'), balance: Number(updatedUser.walletBalance || 0).toLocaleString(
reason: description || (type === 'refund' ? 'استرداد وجه به کیف پول' : 'شارژ کیف پول توسط مدیریت'), 'fa-IR',
),
reason:
description ||
(type === 'refund'
? 'استرداد وجه به کیف پول'
: 'شارژ کیف پول توسط مدیریت'),
date: new Date().toLocaleDateString('fa-IR'), date: new Date().toLocaleDateString('fa-IR'),
}; };

View File

@ -304,12 +304,16 @@ export class ProductDto {
@IsArray() @IsArray()
keyHighlights?: string[]; keyHighlights?: string[];
@ApiPropertyOptional({ description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)' }) @ApiPropertyOptional({
description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)',
})
@IsOptional() @IsOptional()
@IsArray() @IsArray()
keyBenefits?: Array<{ icon: string; title: string; description: string }>; keyBenefits?: Array<{ icon: string; title: string; description: string }>;
@ApiPropertyOptional({ description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)' }) @ApiPropertyOptional({
description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)',
})
@IsOptional() @IsOptional()
analysis?: Record<string, string>; analysis?: Record<string, string>;
@ -327,12 +331,16 @@ export class ProductDto {
@IsOptional() @IsOptional()
contraindications?: string[] | string; contraindications?: string[] | string;
@ApiPropertyOptional({ description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)' }) @ApiPropertyOptional({
description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)',
})
@IsOptional() @IsOptional()
@IsArray() @IsArray()
expectedResults?: Array<{ icon: string; text: string }>; expectedResults?: Array<{ icon: string; text: string }>;
@ApiPropertyOptional({ description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)' }) @ApiPropertyOptional({
description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)',
})
@IsOptional() @IsOptional()
specialist?: { specialist?: {
name?: string; name?: string;

View File

@ -27,39 +27,93 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
{ {
event: 'ORDER_PAID', event: 'ORDER_PAID',
label: 'پرداخت موفق سفارش (Order Paid)', label: 'پرداخت موفق سفارش (Order Paid)',
description: 'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول', description:
'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول',
variables: [ variables: [
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' }, {
key: 'orderNumber',
label: 'شماره سفارش / فاکتور',
sample: 'CN-140308-01',
},
{ key: 'amount', label: 'مبلغ پرداختی (تومان)', sample: '۱,۴۵۰,۰۰۰' }, { key: 'amount', label: 'مبلغ پرداختی (تومان)', sample: '۱,۴۵۰,۰۰۰' },
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' }, { key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' }, {
key: 'customerPhone',
label: 'شماره موبایل خریدار',
sample: '09123456789',
},
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' }, { key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'recipientPhone', label: 'تلفن تماس تحویل‌گیرنده', sample: '09351234567' }, {
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' }, key: 'recipientPhone',
label: 'تلفن تماس تحویل‌گیرنده',
sample: '09351234567',
},
{
key: 'provinceCity',
label: 'استان و شهر مقصد',
sample: 'تهران - تهران',
},
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' }, { key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
{ key: 'address', label: 'نشانی کامل پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' }, {
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' }, key: 'address',
label: 'نشانی کامل پستی',
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
},
{
key: 'itemsSummary',
label: 'خلاصه اقلام سفارش',
sample: 'شیر خشک توله سگ (۲)',
},
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' }, { key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'آنلاین' }, { key: 'paymentMethod', label: 'روش پرداخت', sample: 'آنلاین' },
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' }, { key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
{ key: 'refNumber', label: 'شماره پیگیری پرداخت بانکی', sample: '83920192' }, {
key: 'refNumber',
label: 'شماره پیگیری پرداخت بانکی',
sample: '83920192',
},
], ],
}, },
{ {
event: 'ORDER_CREATED', event: 'ORDER_CREATED',
label: 'ثبت سفارش کارت به کارت یا غیرآنلاین', label: 'ثبت سفارش کارت به کارت یا غیرآنلاین',
description: 'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت می‌شود', description:
'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت می‌شود',
variables: [ variables: [
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' }, {
key: 'orderNumber',
label: 'شماره سفارش / فاکتور',
sample: 'CN-140308-01',
},
{ key: 'amount', label: 'مبلغ کل سفارش', sample: '۱,۴۵۰,۰۰۰' }, { key: 'amount', label: 'مبلغ کل سفارش', sample: '۱,۴۵۰,۰۰۰' },
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' }, { key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' }, {
key: 'customerPhone',
label: 'شماره موبایل خریدار',
sample: '09123456789',
},
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' }, { key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'recipientPhone', label: 'تلفن تحویل‌گیرنده', sample: '09351234567' }, {
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' }, key: 'recipientPhone',
label: 'تلفن تحویل‌گیرنده',
sample: '09351234567',
},
{
key: 'provinceCity',
label: 'استان و شهر مقصد',
sample: 'تهران - تهران',
},
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' }, { key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
{ key: 'address', label: 'نشانی پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' }, {
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' }, key: 'address',
label: 'نشانی پستی',
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
},
{
key: 'itemsSummary',
label: 'خلاصه اقلام سفارش',
sample: 'شیر خشک توله سگ (۲)',
},
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' }, { key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'کارت به کارت' }, { key: 'paymentMethod', label: 'روش پرداخت', sample: 'کارت به کارت' },
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' }, { key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
@ -71,11 +125,23 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
description: 'هنگام تغییر وضعیت سفارش به ارسال شده یا درج کد رهگیری پستی', description: 'هنگام تغییر وضعیت سفارش به ارسال شده یا درج کد رهگیری پستی',
variables: [ variables: [
{ key: 'orderNumber', label: 'شماره سفارش', sample: 'CN-140308-01' }, { key: 'orderNumber', label: 'شماره سفارش', sample: 'CN-140308-01' },
{ key: 'shippingTrackingCode', label: 'کد رهگیری ۲۴ رقمی پست', sample: '184920394857283948571928' }, {
{ key: 'trackingCode', label: 'کد رهگیری پستی (مترادف)', sample: '184920394857283948571928' }, key: 'shippingTrackingCode',
label: 'کد رهگیری ۲۴ رقمی پست',
sample: '184920394857283948571928',
},
{
key: 'trackingCode',
label: 'کد رهگیری پستی (مترادف)',
sample: '184920394857283948571928',
},
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' }, { key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' }, { key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' }, {
key: 'customerPhone',
label: 'شماره همراه مشتری',
sample: '09123456789',
},
{ key: 'shippingMethod', label: 'روش ارسال', sample: 'پست پیشتاز' }, { key: 'shippingMethod', label: 'روش ارسال', sample: 'پست پیشتاز' },
], ],
}, },
@ -84,9 +150,21 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
label: 'درخواست همکاری و عمده‌فروشی B2B', label: 'درخواست همکاری و عمده‌فروشی B2B',
description: 'هنگام ارسال فرم همکاری همکاران و کلینیک‌های دامپزشکی', description: 'هنگام ارسال فرم همکاری همکاران و کلینیک‌های دامپزشکی',
variables: [ variables: [
{ key: 'applicantName', label: 'نام متقاضی / فروشگاه', sample: 'پت‌شاپ آلفا' }, {
{ key: 'applicantPhone', label: 'شماره همراه متقاضی', sample: '09123456789' }, key: 'applicantName',
{ key: 'companyName', label: 'نام شرکت یا مجموعه', sample: 'فروشگاه آلفا' }, label: 'نام متقاضی / فروشگاه',
sample: 'پت‌شاپ آلفا',
},
{
key: 'applicantPhone',
label: 'شماره همراه متقاضی',
sample: '09123456789',
},
{
key: 'companyName',
label: 'نام شرکت یا مجموعه',
sample: 'فروشگاه آلفا',
},
{ key: 'businessType', label: 'نوع فعالیت', sample: 'پت‌شاپ' }, { key: 'businessType', label: 'نوع فعالیت', sample: 'پت‌شاپ' },
{ key: 'date', label: 'تاریخ ثبت درخواست', sample: '۱۴۰۳/۰۶/۱۵' }, { key: 'date', label: 'تاریخ ثبت درخواست', sample: '۱۴۰۳/۰۶/۱۵' },
], ],
@ -97,23 +175,44 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
description: 'هنگامی که کاربری در فرم تماس پیامی ارسال می‌کند', description: 'هنگامی که کاربری در فرم تماس پیامی ارسال می‌کند',
variables: [ variables: [
{ key: 'senderName', label: 'نام فرستنده پیام', sample: 'سارا احمدی' }, { key: 'senderName', label: 'نام فرستنده پیام', sample: 'سارا احمدی' },
{ key: 'senderPhone', label: 'شماره همراه فرستنده', sample: '09123456789' }, {
key: 'senderPhone',
label: 'شماره همراه فرستنده',
sample: '09123456789',
},
{ key: 'email', label: 'ایمیل فرستنده', sample: 'user@example.com' }, { key: 'email', label: 'ایمیل فرستنده', sample: 'user@example.com' },
{ key: 'subject', label: 'موضوع پیام', sample: 'مشاوره خرید مکمل' }, { key: 'subject', label: 'موضوع پیام', sample: 'مشاوره خرید مکمل' },
{ key: 'message', label: 'متن پیام', sample: 'درخواست راهنمایی در مورد دوز کلسیم' }, {
key: 'message',
label: 'متن پیام',
sample: 'درخواست راهنمایی در مورد دوز کلسیم',
},
{ key: 'date', label: 'تاریخ ارسال', sample: '۱۴۰۳/۰۶/۱۵' }, { key: 'date', label: 'تاریخ ارسال', sample: '۱۴۰۳/۰۶/۱۵' },
], ],
}, },
{ {
event: 'WALLET_CHARGED', event: 'WALLET_CHARGED',
label: 'شارژ و افزایش موجودی کیف پول', label: 'شارژ و افزایش موجودی کیف پول',
description: 'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کش‌بک یا استرداد)', description:
'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کش‌بک یا استرداد)',
variables: [ variables: [
{ key: 'customerName', label: 'نام مشتری', sample: 'علی محمدی' }, { key: 'customerName', label: 'نام مشتری', sample: 'علی محمدی' },
{ key: 'amount', label: 'مبلغ شارژ (تومان)', sample: '۵۰,۰۰۰' }, { key: 'amount', label: 'مبلغ شارژ (تومان)', sample: '۵۰,۰۰۰' },
{ key: 'balance', label: 'موجودی جدید کیف پول (تومان)', sample: '۱۵۰,۰۰۰' }, {
{ key: 'reason', label: 'علت / نوع شارژ', sample: 'شارژ آنلاین / پاداش خرید (Cashback)' }, key: 'balance',
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' }, label: 'موجودی جدید کیف پول (تومان)',
sample: '۱۵۰,۰۰۰',
},
{
key: 'reason',
label: 'علت / نوع شارژ',
sample: 'شارژ آنلاین / پاداش خرید (Cashback)',
},
{
key: 'customerPhone',
label: 'شماره همراه مشتری',
sample: '09123456789',
},
{ key: 'date', label: 'تاریخ شارژ', sample: '۱۴۰۳/۰۶/۱۵' }, { key: 'date', label: 'تاریخ شارژ', sample: '۱۴۰۳/۰۶/۱۵' },
], ],
}, },
@ -128,7 +227,8 @@ export const DEFAULT_SMS_RULES: SmsRule[] = [
recipientType: 'customer', recipientType: 'customer',
patternId: 508081, patternId: 508081,
variables: ['customerName', 'orderNumber', 'amount'], variables: ['customerName', 'orderNumber', 'amount'],
description: 'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت', description:
'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت',
}, },
{ {
id: 'default-order-admin', id: 'default-order-admin',

View File

@ -163,9 +163,7 @@ export class SmsService {
const activeRules = (config.rules || []).filter( const activeRules = (config.rules || []).filter(
(rule) => (rule) =>
rule.enabled && rule.enabled && rule.event === event && Number(rule.patternId) > 0,
rule.event === event &&
Number(rule.patternId) > 0,
); );
if (activeRules.length > 0) { if (activeRules.length > 0) {
@ -297,7 +295,8 @@ export class SmsService {
if (!config.username || !config.password) { if (!config.username || !config.password) {
return { return {
success: false, success: false,
message: 'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.', message:
'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.',
}; };
} }
@ -323,7 +322,8 @@ export class SmsService {
if (!phone) { if (!phone) {
return { return {
success: false, success: false,
message: 'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.', message:
'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.',
}; };
} }
@ -496,7 +496,8 @@ export class SmsService {
const patterns: MeliPayamakPattern[] = []; const patterns: MeliPayamakPattern[] = [];
// MeliPayamak uses <ShareServiceBody> and child tags <BodyID>, <Title>, <Body>, <BodyStatus> // MeliPayamak uses <ShareServiceBody> and child tags <BodyID>, <Title>, <Body>, <BodyStatus>
// We match both <ShareServiceBody> and <SharedServiceBody> for backwards/cross compatibility // We match both <ShareServiceBody> and <SharedServiceBody> for backwards/cross compatibility
const itemRegex = /<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi; const itemRegex =
/<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi;
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
while ((match = itemRegex.exec(xml)) !== null) { while ((match = itemRegex.exec(xml)) !== null) {
@ -504,7 +505,9 @@ export class SmsService {
const idMatch = block.match(/<(?:BodyID|Id)>(\d+)<\/(?:BodyID|Id)>/i); const idMatch = block.match(/<(?:BodyID|Id)>(\d+)<\/(?:BodyID|Id)>/i);
const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i); const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i); const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i);
const statusMatch = block.match(/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i); const statusMatch = block.match(
/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i,
);
if (idMatch) { if (idMatch) {
const id = parseInt(idMatch[1], 10); const id = parseInt(idMatch[1], 10);
@ -616,7 +619,9 @@ export class SmsService {
const patterns = await this.getPatterns(); const patterns = await this.getPatterns();
const target = patterns.find((p) => p.id === patternId); const target = patterns.find((p) => p.id === patternId);
if (!target || !target.body) { if (!target || !target.body) {
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`; return args.length > 0
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
: `الگو ${patternId}`;
} }
let rendered = target.body; let rendered = target.body;
@ -626,7 +631,9 @@ export class SmsService {
}); });
return rendered; return rendered;
} catch { } catch {
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`; return args.length > 0
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
: `الگو ${patternId}`;
} }
} }
@ -761,19 +768,34 @@ export class SmsService {
} }
// Check legacy default bindings // Check legacy default bindings
if (p.id === config.otpBodyId && !assigned.some((a) => a.includes('OTP'))) { if (
p.id === config.otpBodyId &&
!assigned.some((a) => a.includes('OTP'))
) {
assigned.push('کد تایید OTP'); assigned.push('کد تایید OTP');
} }
if (p.id === config.orderBodyId && !assigned.some((a) => a.includes('سفارش'))) { if (
p.id === config.orderBodyId &&
!assigned.some((a) => a.includes('سفارش'))
) {
assigned.push('پترن پیش‌فرض سفارش'); assigned.push('پترن پیش‌فرض سفارش');
} }
if (p.id === config.shippingBodyId && !assigned.some((a) => a.includes('پست'))) { if (
p.id === config.shippingBodyId &&
!assigned.some((a) => a.includes('پست'))
) {
assigned.push('پترن پیش‌فرض پست'); assigned.push('پترن پیش‌فرض پست');
} }
if (p.id === config.b2bBodyId && !assigned.some((a) => a.includes('B2B'))) { if (
p.id === config.b2bBodyId &&
!assigned.some((a) => a.includes('B2B'))
) {
assigned.push('پترن پیش‌فرض B2B'); assigned.push('پترن پیش‌فرض B2B');
} }
if (p.id === config.petCareBodyId && !assigned.some((a) => a.includes('پت'))) { if (
p.id === config.petCareBodyId &&
!assigned.some((a) => a.includes('پت'))
) {
assigned.push('پترن پیش‌فرض پرونده پت'); assigned.push('پترن پیش‌فرض پرونده پت');
} }
@ -1029,43 +1051,62 @@ export class SmsService {
}); });
// Pre-render message text for logging // Pre-render message text for logging
void this.renderPatternMessage(options.bodyId, options.args).then((renderedText) => { void this.renderPatternMessage(options.bodyId, options.args).then(
const req = https.request( (renderedText) => {
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber', const req = https.request(
{ 'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
method: 'POST', {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
'Content-Length': Buffer.byteLength(payload), 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
}, },
}, (res) => {
(res) => { let data = '';
let data = ''; res.on(
res.on('data', (chunk: Buffer | string) => (data += String(chunk))); 'data',
res.on('end', () => { (chunk: Buffer | string) => (data += String(chunk)),
void (async () => { );
try { res.on('end', () => {
const json = JSON.parse(data) as MeliPayamakResponse; void (async () => {
const val = json.Value ?? 0; try {
if (json && (val > 15 || json.RetStatus === 1)) { const json = JSON.parse(data) as MeliPayamakResponse;
this.logger.log( const val = json.Value ?? 0;
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`, if (json && (val > 15 || json.RetStatus === 1)) {
); this.logger.log(
await this.recordLog({ `[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
receptor: options.to, );
type: msgType, await this.recordLog({
patternId: options.bodyId, receptor: options.to,
args: options.args, type: msgType,
messageText: renderedText, patternId: options.bodyId,
status: 'SUCCESS', args: options.args,
recId: String(val), messageText: renderedText,
}); status: 'SUCCESS',
resolve(true); recId: String(val),
} else { });
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`; resolve(true);
this.logger.error( } else {
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`, const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
); this.logger.error(
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
recId: String(val),
errorMessage: errMsg,
});
resolve(false);
}
} catch (err: unknown) {
const msg =
err instanceof Error ? err.message : String(err);
await this.recordLog({ await this.recordLog({
receptor: options.to, receptor: options.to,
type: msgType, type: msgType,
@ -1073,50 +1114,37 @@ export class SmsService {
args: options.args, args: options.args,
messageText: renderedText, messageText: renderedText,
status: 'FAILED', status: 'FAILED',
recId: String(val), errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
errorMessage: errMsg,
}); });
resolve(false); resolve(false);
} }
} catch (err: unknown) { })();
const msg = err instanceof Error ? err.message : String(err); });
await this.recordLog({ },
receptor: options.to, );
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
});
resolve(false);
}
})();
});
},
);
req.on('error', (err: Error) => { req.on('error', (err: Error) => {
void (async () => { void (async () => {
this.logger.error( this.logger.error(
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`, `[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
); );
await this.recordLog({ await this.recordLog({
receptor: options.to, receptor: options.to,
type: msgType, type: msgType,
patternId: options.bodyId, patternId: options.bodyId,
args: options.args, args: options.args,
messageText: renderedText, messageText: renderedText,
status: 'FAILED', status: 'FAILED',
errorMessage: `خطای شبکه: ${err.message}`, errorMessage: `خطای شبکه: ${err.message}`,
}); });
resolve(false); resolve(false);
})(); })();
}); });
req.write(payload); req.write(payload);
req.end(); req.end();
}); },
);
}); });
} }

View File

@ -257,11 +257,9 @@ export class OrdersService {
// Send Order Confirmation SMS for wallet orders // Send Order Confirmation SMS for wallet orders
if (user.mobile) { if (user.mobile) {
const orderData = this.buildOrderEventData(createdWalletOrder, user); const orderData = this.buildOrderEventData(createdWalletOrder, user);
this.smsService this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
.triggerEvent('ORDER_PAID', orderData) this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
.catch((err) => { });
this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
});
} }
return createdWalletOrder; return createdWalletOrder;
@ -324,29 +322,36 @@ export class OrdersService {
shipping = {}; shipping = {};
} }
} }
// If not parsed as JSON or fields are missing, extract from formatted address string: // If not parsed as JSON or fields are missing, extract from formatted address string:
// Pattern: "استان، شهر، آدرس پستی (کد پستی: ۱۲۳۴۵۶۷۸۹۰) (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)" // Pattern: "استان، شهر، آدرس پستی (کد پستی: ۱۲۳۴۵۶۷۸۹۰) (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)"
if (!shipping.province || !shipping.address) { if (!shipping.province || !shipping.address) {
let text = trimmed; let text = trimmed;
// 1. Extract recipient & phone: (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰) // 1. Extract recipient & phone: (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/); const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) { if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim(); if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim(); if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim(); text = text.replace(recipientMatch[0], '').trim();
} }
// 2. Extract postal code: (کد پستی: ۱۲۳۴۵۶۷۸۹۰) // 2. Extract postal code: (کد پستی: ۱۲۳۴۵۶۷۸۹۰)
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/); const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) { if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim(); if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim(); text = text.replace(postalMatch[0], '').trim();
} }
// 3. Extract province and city: "استان، شهر، بقیه آدرس" or "استان - شهر - بقیه آدرس" // 3. Extract province and city: "استان، شهر، بقیه آدرس" or "استان - شهر - بقیه آدرس"
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean); const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) { if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0]; if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1]; if (!shipping.city) shipping.city = parts[1];
@ -362,10 +367,7 @@ export class OrdersService {
const items = order.orderItems || []; const items = order.orderItems || [];
const itemsSummary = items const itemsSummary = items
.map( .map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
(i: any) =>
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
)
.join('، '); .join('، ');
const itemCount = items.reduce( const itemCount = items.reduce(
(sum: number, i: any) => sum + (i.quantity || 1), (sum: number, i: any) => sum + (i.quantity || 1),
@ -384,11 +386,7 @@ export class OrdersService {
customerFullName customerFullName
).trim(); ).trim();
const recipientMobile = ( const recipientMobile = (shipping.phone || user?.mobile || '').trim();
shipping.phone ||
user?.mobile ||
''
).trim();
const provinceCity = ( const provinceCity = (
shipping.province && shipping.city shipping.province && shipping.city
@ -447,14 +445,17 @@ export class OrdersService {
const mobile = updatedOrder.user?.mobile; const mobile = updatedOrder.user?.mobile;
const finalTracking = trackingCode || updatedOrder.trackingNumber || ''; const finalTracking = trackingCode || updatedOrder.trackingNumber || '';
if (status === 'shipped' && mobile) { if (status === 'shipped' && mobile) {
const orderData = this.buildOrderEventData(updatedOrder, updatedOrder.user); const orderData = this.buildOrderEventData(
updatedOrder,
updatedOrder.user,
);
orderData.trackingCode = finalTracking || 'ارسال با پیک شهری'; orderData.trackingCode = finalTracking || 'ارسال با پیک شهری';
orderData.shippingTrackingCode = finalTracking || 'ارسال با پیک شهری'; orderData.shippingTrackingCode = finalTracking || 'ارسال با پیک شهری';
orderData.shippingMethod = finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری'; orderData.shippingMethod = finalTracking
? 'پست پیشتاز / تیپاکس'
: 'پیک شهری';
this.smsService this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {});
.triggerEvent('ORDER_SHIPPED', orderData)
.catch(() => {});
} }
return updatedOrder; return updatedOrder;

View File

@ -251,9 +251,10 @@ export class PaymentController {
tryReverse?: boolean; tryReverse?: boolean;
cardNumber?: string; cardNumber?: string;
description?: string; description?: string;
orderId?: string;
}, },
) { ) {
return this.zibalService.requestRefund(body); return this.paymentService.handleAdminGatewayRefund(body);
} }
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)

View File

@ -511,22 +511,31 @@ export class PaymentService {
} }
if (!shipping.province || !shipping.address) { if (!shipping.province || !shipping.address) {
let text = trimmed; let text = trimmed;
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/); const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) { if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim(); if (!shipping.fullName)
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim(); shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim(); text = text.replace(recipientMatch[0], '').trim();
} }
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/); const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) { if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim(); if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim(); text = text.replace(postalMatch[0], '').trim();
} }
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean); const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) { if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0]; if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1]; if (!shipping.city) shipping.city = parts[1];
if (!shipping.address) shipping.address = parts.slice(2).join('، '); if (!shipping.address)
shipping.address = parts.slice(2).join('، ');
} else if (parts.length === 2) { } else if (parts.length === 2) {
if (!shipping.province) shipping.province = parts[0]; if (!shipping.province) shipping.province = parts[0];
if (!shipping.address) shipping.address = parts[1]; if (!shipping.address) shipping.address = parts[1];
@ -538,10 +547,7 @@ export class PaymentService {
const items = order?.orderItems || []; const items = order?.orderItems || [];
const itemsSummary = items const itemsSummary = items
.map( .map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
(i: any) =>
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
)
.join('، '); .join('، ');
const itemCount = items.reduce( const itemCount = items.reduce(
(sum: number, i: any) => sum + (i.quantity || 1), (sum: number, i: any) => sum + (i.quantity || 1),
@ -562,11 +568,7 @@ export class PaymentService {
customerName customerName
).trim(); ).trim();
const recipientMobile = ( const recipientMobile = (shipping.phone || customerMobile || '').trim();
shipping.phone ||
customerMobile ||
''
).trim();
const provinceCity = ( const provinceCity = (
shipping.province && shipping.city shipping.province && shipping.city
@ -601,11 +603,9 @@ export class PaymentService {
orderDate: new Date().toLocaleDateString('fa-IR'), orderDate: new Date().toLocaleDateString('fa-IR'),
}; };
this.smsService this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
.triggerEvent('ORDER_PAID', orderData) this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
.catch((err) => { });
this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
});
} }
// Send SMS notification if wallet top-up // Send SMS notification if wallet top-up
@ -632,7 +632,9 @@ export class PaymentService {
this.smsService this.smsService
.triggerEvent('WALLET_CHARGED', topupData) .triggerEvent('WALLET_CHARGED', topupData)
.catch((err) => { .catch((err) => {
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`); this.logger.warn(
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
);
}); });
} }
} catch (err) { } catch (err) {
@ -1130,7 +1132,9 @@ export class PaymentService {
this.smsService this.smsService
.triggerEvent('WALLET_CHARGED', topupData) .triggerEvent('WALLET_CHARGED', topupData)
.catch((err) => { .catch((err) => {
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`); this.logger.warn(
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
);
}); });
} }
} catch (err) { } catch (err) {
@ -1176,4 +1180,99 @@ export class PaymentService {
async checkGatewayHealth() { async checkGatewayHealth() {
return this.zibalService.checkHealth(); return this.zibalService.checkHealth();
} }
/**
* 12. Admin: Process Gateway Refund (Zibal) with Wallet Balance Deduction if previously refunded to wallet
*/
async handleAdminGatewayRefund(options: {
trackId: string | number;
amount?: number; // Rial
tryReverse?: boolean;
cardNumber?: string;
description?: string;
orderId?: string;
}) {
// 1. If orderId is not passed, attempt to find by trackId
let order = options.orderId
? await this.prisma.order.findUnique({
where: { id: options.orderId },
include: { user: true },
})
: null;
if (!order && options.trackId) {
const tx = await this.prisma.paymentTransaction.findUnique({
where: { trackId: String(options.trackId) },
include: { order: { include: { user: true } } },
});
if (tx?.order) {
order = tx.order;
}
}
if (order && order.refundStatus === 'gateway') {
throw new BadRequestException(
'برای این سفارش قبلاً استرداد درگاه بانکی انجام شده است',
);
}
// 2. Execute refund via Zibal
const zibalRes = await this.zibalService.requestRefund({
trackId: options.trackId,
amount: options.amount,
tryReverse: options.tryReverse,
cardNumber: options.cardNumber,
description: options.description,
});
const isSuccess =
zibalRes?.result === 1 ||
zibalRes?.status === 1 ||
zibalRes?.result === 100 ||
zibalRes?.success === true;
if (isSuccess && order) {
const refundAmountTomans = options.amount
? Math.round(Number(options.amount) / 10)
: Number(order.totalAmount || 0);
await this.prisma.$transaction(async (tx) => {
// If order was previously refunded to wallet, deduct that amount from user's wallet!
if (order.refundStatus === 'wallet' && order.userId) {
const deductionAmount = Number(
order.refundedAmount || refundAmountTomans,
);
await tx.user.update({
where: { id: order.userId },
data: {
walletBalance: { decrement: deductionAmount },
},
});
await tx.walletTransaction.create({
data: {
userId: order.userId,
amount: deductionAmount,
type: 'withdraw',
status: 'completed',
description: `کسر موجودی کیف پول به علت استرداد بانکی سفارش #${order.trackingNumber || order.id.slice(0, 8)} از طریق زیبال`,
},
});
}
// Update order refund status to gateway
await tx.order.update({
where: { id: order.id },
data: {
status: 'cancelled',
refundStatus: 'gateway',
refundedAmount: refundAmountTomans,
refundMethod: 'zibal',
},
});
});
}
return zibalRes;
}
} }

View File

@ -223,7 +223,10 @@ export class SettingsController {
@Roles('Admin') @Roles('Admin')
@ApiBearerAuth() @ApiBearerAuth()
@Get('sms/events') @Get('sms/events')
@ApiOperation({ summary: 'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترن‌ها' }) @ApiOperation({
summary:
'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترن‌ها',
})
getSmsEvents() { getSmsEvents() {
return this.settingsService.getSmsEvents(); return this.settingsService.getSmsEvents();
} }
@ -232,11 +235,10 @@ export class SettingsController {
@Roles('Admin') @Roles('Admin')
@ApiBearerAuth() @ApiBearerAuth()
@Post('sms/test-rule') @Post('sms/test-rule')
@ApiOperation({ summary: 'تست ارسال پیامک برای یک سناریوی تعریف‌شده با دیتای تستی' }) @ApiOperation({
testSmsRule( summary: 'تست ارسال پیامک برای یک سناریوی تعریف‌شده با دیتای تستی',
@Body('rule') rule: any, })
@Body('testPhone') testPhone?: string, testSmsRule(@Body('rule') rule: any, @Body('testPhone') testPhone?: string) {
) {
return this.settingsService.testSmsRule(rule, testPhone); return this.settingsService.testSmsRule(rule, testPhone);
} }

View File

@ -608,8 +608,12 @@ export class SettingsService implements OnModuleInit {
updates['ogImageUrl'] = String(obj.ogImageUrl); updates['ogImageUrl'] = String(obj.ogImageUrl);
} }
if ('googleSiteVerification' in obj) { if ('googleSiteVerification' in obj) {
updates['googleSiteVerification'] = String(obj.googleSiteVerification ?? ''); updates['googleSiteVerification'] = String(
updates['google_site_verification'] = String(obj.googleSiteVerification ?? ''); obj.googleSiteVerification ?? '',
);
updates['google_site_verification'] = String(
obj.googleSiteVerification ?? '',
);
} }
if ('googleAnalyticsId' in obj) { if ('googleAnalyticsId' in obj) {
updates['googleAnalyticsId'] = String(obj.googleAnalyticsId ?? ''); updates['googleAnalyticsId'] = String(obj.googleAnalyticsId ?? '');
@ -620,10 +624,14 @@ export class SettingsService implements OnModuleInit {
updates['gtm_id'] = String(obj.googleTagManagerId ?? ''); updates['gtm_id'] = String(obj.googleTagManagerId ?? '');
} }
if ('enableGoogleAnalytics' in obj) { if ('enableGoogleAnalytics' in obj) {
updates['enableGoogleAnalytics'] = String(Boolean(obj.enableGoogleAnalytics)); updates['enableGoogleAnalytics'] = String(
Boolean(obj.enableGoogleAnalytics),
);
} }
if ('enableGoogleTagManager' in obj) { if ('enableGoogleTagManager' in obj) {
updates['enableGoogleTagManager'] = String(Boolean(obj.enableGoogleTagManager)); updates['enableGoogleTagManager'] = String(
Boolean(obj.enableGoogleTagManager),
);
} }
if ('clarityProjectId' in obj) { if ('clarityProjectId' in obj) {
updates['clarityProjectId'] = String(obj.clarityProjectId ?? ''); updates['clarityProjectId'] = String(obj.clarityProjectId ?? '');
@ -659,7 +667,7 @@ export class SettingsService implements OnModuleInit {
return this.smsService.testSms(targetPhone, patternId, args); return this.smsService.testSms(targetPhone, patternId, args);
} }
async getSmsEvents() { getSmsEvents() {
return this.smsService.getEventDefinitions(); return this.smsService.getEventDefinitions();
} }

View File

@ -309,12 +309,16 @@ export class UsersService {
customerName: customerFullName, customerName: customerFullName,
customerPhone: updatedUser.mobile, customerPhone: updatedUser.mobile,
amount: Number(amount || 0).toLocaleString('fa-IR'), amount: Number(amount || 0).toLocaleString('fa-IR'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'), balance: Number(updatedUser.walletBalance || 0).toLocaleString(
'fa-IR',
),
reason: 'افزایش موجودی کیف پول', reason: 'افزایش موجودی کیف پول',
date: new Date().toLocaleDateString('fa-IR'), date: new Date().toLocaleDateString('fa-IR'),
}; };
this.smsService.triggerEvent('WALLET_CHARGED', topupData).catch(() => {}); this.smsService
.triggerEvent('WALLET_CHARGED', topupData)
.catch(() => {});
} catch (err) { } catch (err) {
// Silently catch SMS error // Silently catch SMS error
} }

View File

@ -102,6 +102,9 @@ export interface Order {
total?: number; total?: number;
isRefill?: boolean; isRefill?: boolean;
shippingAddress?: string; shippingAddress?: string;
refundStatus?: string;
refundedAmount?: number;
refundMethod?: string;
} }
export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = { export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = {
@ -242,8 +245,12 @@ export default function Orders() {
setRefundModalOrder(order); setRefundModalOrder(order);
const amt = Number(order.totalAmount || order.total || 0); const amt = Number(order.totalAmount || order.total || 0);
setRefundAmount(String(amt)); setRefundAmount(String(amt));
// If order was paid by wallet, default to wallet. If online, let admin choose. // If order was already refunded to wallet, default/force to gateway
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal'); if (order.refundStatus === 'wallet') {
setRefundTarget('zibal');
} else {
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
}
setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`); setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`);
updateUrlParams({ modal: 'refund', orderId: order.id }); updateUrlParams({ modal: 'refund', orderId: order.id });
}; };
@ -440,16 +447,15 @@ export default function Orders() {
} }
const res = await api.post('/payment/admin/refund', { const res = await api.post('/payment/admin/refund', {
orderId: refundModalOrder.id,
trackId, trackId,
amount: refundAmount ? Number(refundAmount) * 10 : undefined, amount: refundAmount ? Number(refundAmount) * 10 : undefined,
tryReverse: true, tryReverse: true,
description: refundReason || `استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`, description: refundReason || `استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`,
}); });
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.success) { if (res.data?.result === 1 || res.data?.status === 1 || res.data?.result === 100 || res.data?.success) {
toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد.'); toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد و وضعیت سفارش به‌روزرسانی شد.');
// Also mark order as cancelled
await api.put(`/admin/orders/${refundModalOrder.id}/status`, { status: 'cancelled' });
setRefundModalOrder(null); setRefundModalOrder(null);
await fetchOrders(); await fetchOrders();
} else { } else {
@ -790,6 +796,22 @@ export default function Orders() {
<CreditCard className="w-3 h-3 shrink-0" /> <CreditCard className="w-3 h-3 shrink-0" />
<span className="font-vazir">{payInfo.label}</span> <span className="font-vazir">{payInfo.label}</span>
</span> </span>
{order.refundStatus && (
<span
className={`text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md border flex items-center gap-1 shadow-sm ${
order.refundStatus === 'gateway'
? 'bg-rose-50 text-rose-700 border-rose-200'
: 'bg-amber-50 text-amber-700 border-amber-200'
}`}
>
<Undo2 className="w-3 h-3 shrink-0" />
<span className="font-vazir">
{order.refundStatus === 'gateway'
? 'مسترد به درگاه'
: 'مسترد به کیف‌پول'}
</span>
</span>
)}
{order.isRefill && ( {order.isRefill && (
<span className="text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm"> <span className="text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm">
تمدید ۵٪- تمدید ۵٪-
@ -855,16 +877,16 @@ export default function Orders() {
className="text-gray-600 hover:bg-gray-100" className="text-gray-600 hover:bg-gray-100"
/> />
{(order.status === 'cancelled' || order.status === 'pending_payment') && ( {(order.status === 'cancelled' || order.status === 'pending_payment') && order.refundStatus !== 'gateway' && (
<Button <Button
variant="ghost" variant="ghost"
size="xs" size="xs"
startIcon={Undo2} startIcon={Undo2}
onClick={() => openOrderRefundModal(order)} onClick={() => openOrderRefundModal(order)}
disabled={isProcessing} disabled={isProcessing}
className="text-rose-600 hover:bg-rose-50" className={`hover:bg-rose-50 ${order.refundStatus === 'wallet' ? 'text-amber-700 hover:bg-amber-50' : 'text-rose-600'}`}
> >
استرداد {order.refundStatus === 'wallet' ? 'استرداد بانکی' : 'استرداد'}
</Button> </Button>
)} )}
</div> </div>
@ -1018,17 +1040,15 @@ export default function Orders() {
{(() => { {(() => {
let addrText = selectedOrder.address?.fullAddress || selectedOrder.shippingAddress || 'ثبت نشده'; let addrText = selectedOrder.address?.fullAddress || selectedOrder.shippingAddress || 'ثبت نشده';
let postCode = selectedOrder.address?.postalCode; let postCode = selectedOrder.address?.postalCode;
let prov = selectedOrder.address?.province;
let ct = selectedOrder.address?.city;
if (typeof selectedOrder.shippingAddress === 'string' && selectedOrder.shippingAddress.trim().startsWith('{')) { if (typeof selectedOrder.shippingAddress === 'string' && selectedOrder.shippingAddress.trim().startsWith('{')) {
try { try {
const parsed = JSON.parse(selectedOrder.shippingAddress); const parsed = JSON.parse(selectedOrder.shippingAddress);
addrText = parsed.formatted || `${parsed.province || ''}، ${parsed.city || ''}، ${parsed.address || ''}`; addrText = parsed.formatted || `${parsed.province || ''}، ${parsed.city || ''}، ${parsed.address || ''}`;
if (parsed.postalCode) postCode = parsed.postalCode; if (parsed.postalCode) postCode = parsed.postalCode;
if (parsed.province) prov = parsed.province; } catch {
if (parsed.city) ct = parsed.city; // ignore malformed json
} catch {} }
} }
return ( return (
@ -1115,6 +1135,16 @@ export default function Orders() {
<span>روش پرداخت:</span> <span>روش پرداخت:</span>
<span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span> <span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span>
</div> </div>
{selectedOrder.refundStatus && (
<div className="flex justify-between font-bold text-amber-300 pt-1 border-t border-gray-800/80">
<span>وضعیت استرداد وجه:</span>
<span>
{selectedOrder.refundStatus === 'gateway'
? `مسترد به درگاه بانکی (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`
: `مسترد به کیف‌پول (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`}
</span>
</div>
)}
<div className="flex justify-between text-sm sm:text-base font-black text-amber-400 pt-2 border-t border-gray-800"> <div className="flex justify-between text-sm sm:text-base font-black text-amber-400 pt-2 border-t border-gray-800">
<span>مبلغ نهایی پرداخت شده:</span> <span>مبلغ نهایی پرداخت شده:</span>
<span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span> <span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span>
@ -1157,14 +1187,22 @@ export default function Orders() {
{/* Destination selector */} {/* Destination selector */}
<div> <div>
<label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label> <label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label>
{refundModalOrder.refundStatus === 'wallet' && (
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded-xl text-amber-800 text-[11px] leading-relaxed">
<strong>توجه:</strong> وجه این سفارش قبلاً به کیف پول کاربر عودت داده شده است. در صورت ثبت استرداد درگاه زیبال، مبلغ استرداد به صورت خودکار از موجودی کیف پول کاربر کسر خواهد شد تا از استرداد مضاعف جلوگیری شود.
</div>
)}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
disabled={refundModalOrder.refundStatus === 'wallet'}
onClick={() => setRefundTarget('wallet')} onClick={() => setRefundTarget('wallet')}
className={`p-3.5 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${ className={`p-3.5 rounded-2xl border text-right transition-all flex flex-col gap-1.5 ${
refundTarget === 'wallet' refundModalOrder.refundStatus === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20' ? 'opacity-40 cursor-not-allowed bg-gray-50 border-gray-200'
: 'border-gray-200 hover:border-gray-300' : refundTarget === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20 cursor-pointer'
: 'border-gray-200 hover:border-gray-300 cursor-pointer'
}`} }`}
> >
<div className="flex items-center gap-2 font-black text-gray-900"> <div className="flex items-center gap-2 font-black text-gray-900">
@ -1172,7 +1210,9 @@ export default function Orders() {
<span>شارژ کیف پول کاربر</span> <span>شارژ کیف پول کاربر</span>
</div> </div>
<p className="text-[11px] text-gray-500 font-medium leading-relaxed"> <p className="text-[11px] text-gray-500 font-medium leading-relaxed">
افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری {refundModalOrder.refundStatus === 'wallet'
? 'قبلاً به کیف پول مسترد شده است'
: 'افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری'}
</p> </p>
</button> </button>

File diff suppressed because it is too large Load Diff

View File

@ -59,8 +59,8 @@ export default function AnalyticsDeferred({ gtmId, gaId, clarityId }: AnalyticsD
(function (c: any, l: any, a: any, r: any, i: any) { (function (c: any, l: any, a: any, r: any, i: any) {
c[a] = c[a] =
c[a] || c[a] ||
function () { function (...args: unknown[]) {
(c[a].q = c[a].q || []).push(arguments); (c[a].q = c[a].q || []).push(args);
}; };
const t = l.createElement(r); const t = l.createElement(r);
t.async = 1; t.async = 1;

View File

@ -386,11 +386,11 @@ export default function OrderDetailsModal({
)} )}
{/* Content Scrollable */} {/* Content Scrollable */}
<div className="max-h-[60vh] overflow-y-auto px-6 sm:px-10 py-6 space-y-6 custom-scrollbar"> <div className="max-h-[65vh] overflow-y-auto px-6 sm:px-10 py-6 space-y-6 custom-scrollbar">
{/* Summary Stats */} {/* Summary Stats */}
<div className="grid grid-cols-3 gap-3 sm:gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<div className="bg-medical-gray-50 p-4 rounded-2xl"> <div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1"> <div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
وضعیت سفارش وضعیت سفارش
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -398,155 +398,178 @@ export default function OrderDetailsModal({
<span className="text-xs sm:text-sm font-black text-medical-gray-900"> <span className="text-xs sm:text-sm font-black text-medical-gray-900">
{getStatusLabel(orderStatus)} {getStatusLabel(orderStatus)}
</span> </span>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-canina-blue" />
سبد اقلام سفارش
</h4>
<div className="space-y-3">
{items.map((item: Record<string, unknown>, idx: number) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const itemQty = Number(item.quantity || 1);
const pSlug = String(prod?.slug || prod?.id || '');
return (
<div
key={String(prod?.id || idx)}
className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all"
>
<div className="flex items-center gap-4">
<div
onClick={() => setZoomedImage(pImg)}
className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1 flex items-center justify-center shrink-0 border border-medical-gray-200 cursor-zoom-in hover:scale-105 transition-transform overflow-hidden"
title="مشاهده تصویر بزرگ"
>
<SafeImage
src={pImg}
alt={pName}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
</div>
<div>
{pSlug ? (
<Link
href={`/shop/${pSlug}`}
onClick={onClose}
className="font-black text-medical-gray-900 hover:text-canina-blue transition-colors text-xs sm:text-sm inline-block font-vazir"
>
{pName}
</Link>
) : (
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm font-vazir">
{pName}
</h5>
)}
<p className="text-xs text-medical-gray-400 mt-0.5 font-vazir">
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
</p>
</div>
</div>
<div className="text-left font-black text-canina-blue text-xs sm:text-sm font-vazir">
{toPersian((pPrice * itemQty).toLocaleString())} تومان
</div>
</div>
);
})}
</div>
</div>
{/* Shipping & Payment */}
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4 text-canina-blue" />
اطلاعات تحویل و آدرس
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2 text-xs font-bold text-medical-gray-700">
<div>
<span className="text-medical-gray-400">گیرنده: </span>
{String(shippingAddress.recipientName || shippingAddress.name || 'کاربر')}
</div>
<div>
<span className="text-medical-gray-400">شماره تماس: </span>
<span className="font-mono">{toPersian(String(shippingAddress.phone || shippingAddress.mobile || '-'))}</span>
</div>
<div>
<span className="text-medical-gray-400">نشانی کامل: </span>
{String(shippingAddress.fullAddress || shippingAddress.address || '-')}
</div>
{Boolean(shippingAddress.postalCode) && (
<div>
<span className="text-medical-gray-400">کد پستی: </span>
<span className="font-mono">{toPersian(String(shippingAddress.postalCode))}</span>
</div>
)}
</div>
</div>
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-canina-blue" />
اطلاعات و وضعیت پرداخت
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2.5 text-xs font-bold text-medical-gray-700">
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">شیوه پرداخت:</span>
<span className="text-medical-gray-900">{getPaymentMethodTitle(order.paymentMethod)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">مجموع اقلام:</span>
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
{charityAmount > 0 && (
<div className="flex justify-between items-center text-pink-500">
<span>ردپای مهربانی:</span>
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">هزینه ارسال:</span>
<span className="text-green-600">رایگان</span>
</div>
<div className="pt-2 border-t border-medical-gray-200/60 flex justify-between items-center text-sm font-black text-medical-gray-900">
<span>مبلغ نهایی فاکتور:</span>
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
{/* Footer Actions */} <div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="p-6 sm:px-10 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row items-center justify-between gap-4"> <div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
<button کد رهگیری سفارش
onClick={handlePrint} </div>
className="w-full sm:w-auto px-5 py-2.5 bg-white border border-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-black hover:border-canina-blue hover:text-canina-blue transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer" <div className="text-xs sm:text-sm font-mono font-black text-canina-blue">
> {toPersian(displayTrackingCode)}
<Download className="w-4 h-4" /> </div>
<span>چاپ فاکتور رسمی</span> </div>
</button>
<div className="flex items-center gap-3 w-full sm:w-auto"> <div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
{isPendingPayment && ( <div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
<button زمان ثبت سفارش
onClick={() => setRetryModalOpen(true)} </div>
className="w-full sm:w-auto px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-xs font-black shadow-lg shadow-green-600/20 transition-all flex items-center justify-center gap-2 cursor-pointer" <div className="flex items-center gap-1.5 text-xs sm:text-sm font-black text-medical-gray-900">
<Clock className="w-4 h-4 text-medical-gray-400" />
<span>{toPersian(formattedTime)} - {toPersian(formattedDate)}</span>
</div>
</div>
</div>
{/* Items Section */}
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-canina-blue" />
سبد اقلام سفارش
</h4>
<div className="space-y-3">
{items.map((item: Record<string, unknown>, idx: number) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const itemQty = Number(item.quantity || 1);
const pSlug = String(prod?.slug || prod?.id || '');
return (
<div
key={String(prod?.id || idx)}
className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all"
> >
<CreditCard className="w-4 h-4" /> <div className="flex items-center gap-4">
<span>پرداخت سفارش</span> <div
</button> onClick={() => setZoomedImage(pImg)}
className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1 flex items-center justify-center shrink-0 border border-medical-gray-200 cursor-zoom-in hover:scale-105 transition-transform overflow-hidden"
title="مشاهده تصویر بزرگ"
>
<SafeImage
src={pImg}
alt={pName}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
</div>
<div>
{pSlug ? (
<Link
href={`/shop/${pSlug}`}
onClick={onClose}
className="font-black text-medical-gray-900 hover:text-canina-blue transition-colors text-xs sm:text-sm inline-block font-vazir"
>
{pName}
</Link>
) : (
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm font-vazir">
{pName}
</h5>
)}
<p className="text-xs text-medical-gray-400 mt-0.5 font-vazir">
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
</p>
</div>
</div>
<div className="text-left font-black text-canina-blue text-xs sm:text-sm font-vazir">
{toPersian((pPrice * itemQty).toLocaleString())} تومان
</div>
</div>
);
})}
</div>
</div>
{/* Shipping & Payment */}
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4 text-canina-blue" />
اطلاعات تحویل و آدرس
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2 text-xs font-bold text-medical-gray-700">
<div>
<span className="text-medical-gray-400">گیرنده: </span>
{String(shippingAddress.recipientName || shippingAddress.name || 'کاربر')}
</div>
<div>
<span className="text-medical-gray-400">شماره تماس: </span>
<span className="font-mono">{toPersian(String(shippingAddress.phone || shippingAddress.mobile || '-'))}</span>
</div>
<div>
<span className="text-medical-gray-400">نشانی کامل: </span>
{String(shippingAddress.fullAddress || shippingAddress.address || '-')}
</div>
{Boolean(shippingAddress.postalCode) && (
<div>
<span className="text-medical-gray-400">کد پستی: </span>
<span className="font-mono">{toPersian(String(shippingAddress.postalCode))}</span>
</div>
)} )}
<button
onClick={onClose}
className="w-full sm:w-auto px-6 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-xl text-xs font-black transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<span>بازگشت</span>
<X className="w-4 h-4" />
</button>
</div> </div>
</div> </div>
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-canina-blue" />
اطلاعات و وضعیت پرداخت
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2.5 text-xs font-bold text-medical-gray-700">
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">شیوه پرداخت:</span>
<span className="text-medical-gray-900">{getPaymentMethodTitle(order.paymentMethod)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">مجموع اقلام:</span>
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
{charityAmount > 0 && (
<div className="flex justify-between items-center text-pink-500">
<span>ردپای مهربانی:</span>
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">هزینه ارسال:</span>
<span className="text-green-600">رایگان</span>
</div>
<div className="pt-2 border-t border-medical-gray-200/60 flex justify-between items-center text-sm font-black text-medical-gray-900">
<span>مبلغ نهایی فاکتور:</span>
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="p-6 sm:px-10 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row items-center justify-between gap-4">
<button
onClick={handlePrint}
className="w-full sm:w-auto px-5 py-2.5 bg-white border border-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-black hover:border-canina-blue hover:text-canina-blue transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer"
>
<Download className="w-4 h-4" />
<span>چاپ فاکتور رسمی</span>
</button>
<div className="flex items-center gap-3 w-full sm:w-auto">
{isPendingPayment && (
<button
onClick={() => setRetryModalOpen(true)}
className="w-full sm:w-auto px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-xs font-black shadow-lg shadow-green-600/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<CreditCard className="w-4 h-4" />
<span>پرداخت سفارش</span>
</button>
)}
<button
onClick={onClose}
className="w-full sm:w-auto px-6 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-xl text-xs font-black transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<span>بازگشت</span>
<X className="w-4 h-4" />
</button>
</div> </div>
</div> </div>

View File

@ -55,6 +55,16 @@ export default function ProductImageZoomModal({
}; };
}, [isOpen, activeImage, resetZoom]); }, [isOpen, activeImage, resetZoom]);
const navigateImage = useCallback((direction: number) => {
if (!images || images.length <= 1) return;
const currentIndex = images.indexOf(activeImage);
if (currentIndex === -1) return;
let nextIndex = (currentIndex + direction) % images.length;
if (nextIndex < 0) nextIndex = images.length - 1;
onSelectImage(images[nextIndex]);
resetZoom();
}, [images, activeImage, onSelectImage, resetZoom]);
// Keyboard navigation & zoom shortcuts // Keyboard navigation & zoom shortcuts
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
@ -81,7 +91,7 @@ export default function ProductImageZoomModal({
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose, images, activeImage, resetZoom]); }, [isOpen, onClose, navigateImage, resetZoom]);
const handleZoomIn = () => { const handleZoomIn = () => {
setScale((prev) => Math.min(prev + 0.5, 4)); setScale((prev) => Math.min(prev + 0.5, 4));
@ -158,15 +168,7 @@ export default function ProductImageZoomModal({
} }
}; };
const navigateImage = (direction: number) => {
if (!images || images.length <= 1) return;
const currentIndex = images.indexOf(activeImage);
if (currentIndex === -1) return;
let nextIndex = (currentIndex + direction) % images.length;
if (nextIndex < 0) nextIndex = images.length - 1;
onSelectImage(images[nextIndex]);
resetZoom();
};
if (!isOpen) return null; if (!isOpen) return null;

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react"; import { motion, AnimatePresence } from "motion/react";
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles, MessageSquare, Send, Stethoscope, FileText, Lock, Key, Eye, EyeOff, ShieldCheck, Dog, X, Plus, CreditCard, Activity } from "lucide-react"; import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles, MessageSquare, Send, Stethoscope, FileText, Lock, Key, Eye, EyeOff, ShieldCheck, Dog, X, Plus, CreditCard, Activity, RotateCcw } from "lucide-react";
import { OrderRowSkeleton } from "./Skeleton"; import { OrderRowSkeleton } from "./Skeleton";
import { useUserStore, Address } from "../lib/store/userStore"; import { useUserStore, Address } from "../lib/store/userStore";
import { useCartStore } from "../lib/store/cartStore"; import { useCartStore } from "../lib/store/cartStore";
@ -987,37 +987,104 @@ export default function UserDashboard() {
<p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p> <p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p>
</div> </div>
) : ( ) : (
(profile?.transactions || []).map((trx) => ( (profile?.transactions || []).map((trx) => {
<div key={trx.id} className="p-4 sm:p-6 bg-white border border-medical-gray-100 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-6 hover:shadow-lg hover:border-canina-blue/10 transition-all group"> const desc = trx.description || "";
<div className="flex items-center gap-3 sm:gap-4"> const isRefund = trx.type === 'refund' || desc.includes('استرداد') || desc.includes('لغو') || desc.includes('عودت');
<div className={cn( const isAdmin = desc.includes('مدیر') || desc.includes('دستی');
"w-10 h-10 sm:w-12 sm:h-12 rounded-xl sm:rounded-2xl flex items-center justify-center transition-colors flex-shrink-0", const isOnlineTopUp = trx.type === 'top_up' && !isRefund && !isAdmin;
trx.type === 'top_up' ? "bg-green-50 text-green-500 group-hover:bg-green-500 group-hover:text-white" : "bg-red-50 text-red-500 group-hover:bg-red-500 group-hover:text-white" const isPurchase = trx.type === 'purchase' || desc.includes('پرداخت سفارش');
)}>
{trx.type === 'top_up' ? <ArrowUpCircle className="w-5 h-5 sm:w-6 sm:h-6" /> : <ShoppingCart className="w-5 h-5 sm:w-6 sm:h-6" />} let title = "تراکنش کیف پول";
</div> let badgeText = "تراکنش";
<div className="min-w-0"> let badgeClass = "bg-medical-gray-100 text-medical-gray-600 border-medical-gray-200";
<div className="text-xs sm:text-sm font-black text-medical-gray-900 truncate">{trx.type === 'top_up' ? "افزایش موجودی (شارژ)" : "پرداخت سفارش"}</div> let iconBgClass = "bg-medical-gray-50 text-medical-gray-600 group-hover:bg-medical-gray-700 group-hover:text-white";
<div className="text-[10px] font-bold text-medical-gray-400 flex flex-wrap items-center gap-1 sm:gap-2 mt-0.5"> let IconComponent = ArrowUpCircle;
<span className="truncate">کد: {trx.id}</span> let isPositive = trx.type === 'top_up' || trx.type === 'refund';
<span className="w-1 h-1 bg-medical-gray-200 rounded-full hidden sm:inline-block" />
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span> if (isRefund) {
</div> title = "استرداد وجه سفارش";
</div> badgeText = "استرداد وجه";
</div> badgeClass = "bg-blue-50 text-blue-700 border-blue-200/60";
<div className="text-right sm:text-left flex sm:flex-col justify-between items-center sm:items-end border-t sm:border-t-0 pt-2 sm:pt-0 border-medical-gray-50"> iconBgClass = "bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white";
<div className={cn( IconComponent = RotateCcw;
"text-base sm:text-lg font-black italic", isPositive = true;
trx.type === 'top_up' ? "text-green-600" : "text-red-500" } else if (isAdmin) {
)}> title = isPositive ? "شارژ توسط مدیریت" : "کسر توسط مدیریت";
{trx.type === 'top_up' ? "+" : ""}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span> badgeText = "تغییر توسط پشتیبانی";
</div> badgeClass = "bg-amber-50 text-amber-700 border-amber-200/60";
<div className="text-[9px] font-black uppercase text-green-500 tracking-widest sm:mt-1"> iconBgClass = "bg-amber-50 text-amber-600 group-hover:bg-amber-600 group-hover:text-white";
تایید شده IconComponent = ShieldCheck;
</div> } else if (isOnlineTopUp) {
</div> title = "افزایش اعتبار آنلاین (درگاه پرداخت)";
</div> badgeText = "شارژ درگاه";
)) badgeClass = "bg-emerald-50 text-emerald-700 border-emerald-200/60";
iconBgClass = "bg-emerald-50 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white";
IconComponent = ArrowUpCircle;
isPositive = true;
} else if (isPurchase) {
title = "پرداخت سفارش از کیف پول";
badgeText = "پرداخت سفارش";
badgeClass = "bg-rose-50 text-rose-700 border-rose-200/60";
iconBgClass = "bg-rose-50 text-rose-600 group-hover:bg-rose-600 group-hover:text-white";
IconComponent = ShoppingCart;
isPositive = false;
}
return (
<div key={trx.id} className="p-4 sm:p-6 bg-white border border-medical-gray-100 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-6 hover:shadow-lg hover:border-canina-blue/20 transition-all group">
<div className="flex items-start sm:items-center gap-3 sm:gap-4 flex-1 min-w-0">
<div className={cn(
"w-10 h-10 sm:w-12 sm:h-12 rounded-xl sm:rounded-2xl flex items-center justify-center transition-colors flex-shrink-0 mt-0.5 sm:mt-0",
iconBgClass
)}>
<IconComponent className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="text-xs sm:text-sm font-black text-medical-gray-900">{title}</span>
<span className={cn("text-[10px] px-2.5 py-0.5 rounded-full border font-black", badgeClass)}>
{badgeText}
</span>
</div>
{desc && (
<p className="text-xs font-bold text-medical-gray-700 line-clamp-2 leading-relaxed mb-1.5">
{desc}
</p>
)}
<div className="text-[10px] font-bold text-medical-gray-400 flex flex-wrap items-center gap-x-2.5 gap-y-1">
<span className="font-mono text-medical-gray-500">کد: {trx.id}</span>
{trx.transactionReference && (
<>
<span className="w-1 h-1 bg-medical-gray-200 rounded-full" />
<span className="font-mono text-canina-blue">مرجع/پیگیری: {trx.transactionReference}</span>
</>
)}
<span className="w-1 h-1 bg-medical-gray-200 rounded-full hidden sm:inline-block" />
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
</div>
</div>
</div>
<div className="text-right sm:text-left flex sm:flex-col justify-between items-center sm:items-end border-t sm:border-t-0 pt-3 sm:pt-0 border-medical-gray-50 flex-shrink-0">
<div className={cn(
"text-base sm:text-lg font-black italic",
isPositive ? "text-emerald-600" : "text-rose-500"
)}>
{isPositive ? "+" : "-"}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
</div>
<div className={cn(
"text-[10px] font-black tracking-wide sm:mt-1 px-2 py-0.5 rounded-full",
trx.status === 'failed' ? "bg-rose-50 text-rose-600" :
trx.status === 'pending' ? "bg-amber-50 text-amber-600" :
"bg-emerald-50 text-emerald-600"
)}>
{trx.status === 'failed' ? "ناموفق" : trx.status === 'pending' ? "در انتظار تایید" : "موفق"}
</div>
</div>
</div>
);
})
)} )}
</div> </div>
</div> </div>

View File

@ -24,10 +24,12 @@ export interface Address {
export interface Transaction { export interface Transaction {
id: string; id: string;
type: 'top_up' | 'purchase'; type: 'top_up' | 'purchase' | 'refund';
amount: number; amount: number;
date: string; date: string;
status: 'success' | 'failed' | 'pending'; status: 'success' | 'failed' | 'pending';
description?: string;
transactionReference?: string;
} }
interface UserProfile { interface UserProfile {
@ -237,12 +239,14 @@ export const useUserStore = create<UserStore>()(
const pd = profileData as unknown as Record<string, unknown>; const pd = profileData as unknown as Record<string, unknown>;
const backendWallet = Number(profileData.walletBalance || 0); const backendWallet = Number(profileData.walletBalance || 0);
const backendCharity = Number(profileData.charityDonationTotal || 0); const backendCharity = Number(profileData.charityDonationTotal || 0);
const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string }>)?.map((t) => ({ const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string; description?: string; transactionReference?: string }>)?.map((t) => ({
id: t.id, id: t.id,
type: (t.type === 'deposit' ? 'top_up' : 'purchase') as Transaction['type'], type: (t.type === 'deposit' ? 'top_up' : t.type === 'refund' ? 'refund' : 'purchase') as Transaction['type'],
amount: Number(t.amount), amount: Number(t.amount),
date: t.createdAt, date: t.createdAt,
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status'] status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status'],
description: t.description || undefined,
transactionReference: t.transactionReference || undefined
})) || []; })) || [];
return { return {

View File

@ -1,16 +1,16 @@
{ {
"0": "Roles", "0": "Roles",
"1": "app.module.ts", "1": "app.module.ts",
"2": "AdminTransactionFilterDto", "2": "payment.service.ts",
"3": "productService.ts", "3": "productService.ts",
"4": "PetProfile.tsx", "4": "UserDashboard.tsx",
"5": "CmsController", "5": "CmsController",
"6": "TicketsService", "6": "tickets.controller.ts",
"7": "SmsSettingsPage.tsx", "7": "SmsSettingsPage.tsx",
"8": "SettingsController", "8": "SmsService",
"9": "devDependencies", "9": "devDependencies",
"10": "CreateReviewDto", "10": "ReviewsService",
"11": "WikiController", "11": "ConfirmModal.tsx",
"12": "index.ts", "12": "index.ts",
"13": "app-audit-verification.e2e-spec.js", "13": "app-audit-verification.e2e-spec.js",
"14": "toPersian", "14": "toPersian",
@ -20,8 +20,8 @@
"18": "JwtAuthGuard", "18": "JwtAuthGuard",
"19": "admin.controller.ts", "19": "admin.controller.ts",
"20": "CreateVideoDto", "20": "CreateVideoDto",
"21": ".getSmsConfig", "21": "auth.service.ts",
"22": "BlogsService", "22": "ProductDto",
"23": "MenuService", "23": "MenuService",
"24": "BE-001", "24": "BE-001",
"25": "FE-001", "25": "FE-001",
@ -47,26 +47,26 @@
"45": "What You Must Do When Invoked", "45": "What You Must Do When Invoked",
"46": "20260526145407_init/migration.sql", "46": "20260526145407_init/migration.sql",
"47": "IngredientsService", "47": "IngredientsService",
"48": "auth.controller.ts", "48": "AuthController",
"49": "devDependencies", "49": "devDependencies",
"50": "devDependencies", "50": "devDependencies",
"51": "BlogsController", "51": "BlogsController",
"52": "PrescriptionsController", "52": "PrescriptionsService",
"53": "SmartAdvisorService", "53": "SmartAdvisorService",
"54": "Button.tsx", "54": "Button.tsx",
"55": "UITexts.tsx", "55": "UITexts.tsx",
"56": "Orders.tsx", "56": "Orders.tsx",
"57": "Role & Core Objective", "57": "Role & Core Objective",
"58": "UserDashboard.tsx", "58": "AuthService",
"59": "compilerOptions", "59": "compilerOptions",
"60": "CreateUserDto", "60": "CreateUserDto",
"61": "ProductPage.tsx", "61": "ProductPage.tsx",
"62": "WikiService", "62": "torob.controller.ts",
"63": "dependencies", "63": "dependencies",
"64": "compilerOptions", "64": "compilerOptions",
"65": "admin.service.ts", "65": "admin.service.ts",
"66": "AdminQueryDto", "66": "AdminQueryDto",
"67": "ReportsController", "67": "admin.module.ts",
"68": "useSettingsStore", "68": "useSettingsStore",
"69": "Required Review Group Closures", "69": "Required Review Group Closures",
"70": "compilerOptions", "70": "compilerOptions",
@ -84,17 +84,17 @@
"82": "scripts", "82": "scripts",
"83": "dependencies", "83": "dependencies",
"84": "Role & Core Objective", "84": "Role & Core Objective",
"85": "auth.module.ts", "85": "RegisterDto",
"86": "zibal.service.ts", "86": "zibal.service.ts",
"87": "dependencies", "87": "dependencies",
"88": "payment.controller.ts", "88": "CreateEBankCheckoutDto",
"89": "seed-products.ts", "89": "seed-products.ts",
"90": "OrderService", "90": "useCartStore",
"91": "Reconciled Audit Roles & Assignments", "91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService", "92": "OrdersService",
"93": "ArchivePage.tsx", "93": "ArchivePage.tsx",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique", "94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "wiki/[slug]/page.tsx", "95": "getSeoConfig",
"96": "compilerOptions", "96": "compilerOptions",
"97": "PaymentService", "97": "PaymentService",
"98": "scripts", "98": "scripts",
@ -105,7 +105,7 @@
"103": "Comprehensive Change Log", "103": "Comprehensive Change Log",
"104": "Products.tsx", "104": "Products.tsx",
"105": "Operational Rules & Boundaries", "105": "Operational Rules & Boundaries",
"106": "cms.controller.ts", "106": "InitiatePaymentDto",
"107": "PaginationDto", "107": "PaginationDto",
"108": "PrismaService", "108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed", "109": "1. Summary of Integrity Repairs Performed",
@ -121,13 +121,13 @@
"119": "compilerOptions", "119": "compilerOptions",
"120": "compilerOptions", "120": "compilerOptions",
"121": "backend/README.md", "121": "backend/README.md",
"122": "AdminController", "122": "AdminService",
"123": "UsersService", "123": "UsersService",
"124": "Repository Map", "124": "Repository Map",
"125": "validate_integrity.js", "125": "validate_integrity.js",
"126": "admin-panel/package.json", "126": "admin-panel/package.json",
"127": "Sahel-Font", "127": "Sahel-Font",
"128": "AdminService", "128": "Body",
"129": "Reports.tsx", "129": "Reports.tsx",
"130": "Sahel-Font", "130": "Sahel-Font",
"131": "Role & Core Objective", "131": "Role & Core Objective",
@ -143,15 +143,15 @@
"141": "application/package.json", "141": "application/package.json",
"142": "start-dev.js", "142": "start-dev.js",
"143": "generate-openapi.js", "143": "generate-openapi.js",
"144": "SafeImage.tsx", "144": "PodcastPlayerModal.tsx",
"145": "AuthService", "145": "AdminLoginDto",
"146": "System Discovery", "146": "System Discovery",
"147": "HomeController", "147": "HomeController",
"148": "track/page.tsx", "148": "VerifyOtpDto",
"149": "trust-seals/page.tsx", "149": "wiki/[slug]/page.tsx",
"150": "Product Requirement Document (PRD)", "150": "Product Requirement Document (PRD)",
"151": "menu.module.ts", "151": "@types/node",
"152": "auth.service.ts", "152": "RevalidationService",
"153": "exclude", "153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History", "154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts", "155": "seo-backfill.ts",
@ -183,8 +183,8 @@
"181": "⚙️ Backend Technical Review (05_dev_backend)", "181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)", "182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)", "183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController", "184": "AppModule",
"185": "RouteErrorBoundary", "185": "eslint-config-next",
"186": "seed-ui-texts.ts", "186": "seed-ui-texts.ts",
"187": "seed-wiki.ts", "187": "seed-wiki.ts",
"188": "update-blog.dto.ts", "188": "update-blog.dto.ts",
@ -203,7 +203,6 @@
"201": "deploy.sh", "201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)", "202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)", "203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "zibal-ebank.service.ts",
"205": "prisma/scientificTerms.ts", "205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts", "206": "seed-blogs.ts",
"207": "seed-custom.ts", "207": "seed-custom.ts",
@ -219,11 +218,9 @@
"217": "sync_honest_manifest.js", "217": "sync_honest_manifest.js",
"218": "sync_manifest.js", "218": "sync_manifest.js",
"219": "FormField.tsx", "219": "FormField.tsx",
"220": "jest",
"221": "Textarea.tsx", "221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json", "222": "admin-panel/tsconfig.json",
"223": "tailwindcss", "223": "tailwindcss",
"224": "class-transformer",
"225": "next.config.ts", "225": "next.config.ts",
"226": "Shabnam Font README", "226": "Shabnam Font README",
"227": "AGENTS.md", "227": "AGENTS.md",
@ -232,7 +229,6 @@
"230": "instructions.md", "230": "instructions.md",
"231": "@nestjs/schematics", "231": "@nestjs/schematics",
"232": "prisma", "232": "prisma",
"233": "Reviews.tsx",
"234": "source-map-support", "234": "source-map-support",
"235": "ts-loader", "235": "ts-loader",
"236": "ts-node", "236": "ts-node",
@ -264,7 +260,6 @@
"262": "User Logout API", "262": "User Logout API",
"263": "generate-openapi.d.ts", "263": "generate-openapi.d.ts",
"264": "@types/compression", "264": "@types/compression",
"265": "helmet",
"266": "@types/express", "266": "@types/express",
"267": "@types/jest", "267": "@types/jest",
"268": "@types/multer", "268": "@types/multer",
@ -294,23 +289,14 @@
"292": "Production Docker Compose", "292": "Production Docker Compose",
"293": "Staging Docker Compose", "293": "Staging Docker Compose",
"294": "ZibalService", "294": "ZibalService",
"295": "js-yaml",
"296": "@nestjs/core",
"297": "ZibalEBankService", "297": "ZibalEBankService",
"298": ".initiateOrderPayment", "298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss", "299": "@tailwindcss/postcss",
"300": "typescript", "300": "typescript",
"301": "@nestjs/jwt",
"302": "typescript-eslint", "302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
"305": "reflect-metadata",
"306": "@eslint/js", "306": "@eslint/js",
"307": "swagger-ui-express",
"308": "eslint-config-prettier",
"309": "axios", "309": "axios",
"310": "tailwindcss", "310": "tailwindcss",
"311": "@eslint/eslintrc",
"312": "@types/passport-jwt", "312": "@types/passport-jwt",
"313": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql", "313": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"314": "eslint-plugin-prettier", "314": "eslint-plugin-prettier",
@ -318,17 +304,9 @@
"316": "globals", "316": "globals",
"317": "revalidate/route.ts", "317": "revalidate/route.ts",
"318": "MaskableField.tsx", "318": "MaskableField.tsx",
"319": "@nestjs/cli",
"320": "@nestjs/testing",
"321": "orders/page.tsx", "321": "orders/page.tsx",
"322": "pets/page.tsx", "322": "pets/page.tsx",
"323": "prettier",
"324": "eslint",
"325": "@types/react-dom", "325": "@types/react-dom",
"326": "@types/js-yaml",
"327": "eslint-plugin-react-refresh", "327": "eslint-plugin-react-refresh",
"328": "@types/supertest", "329": "typescript-eslint"
"329": "typescript-eslint",
"330": "@nestjs/swagger",
"331": "tailwindcss"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,334 @@
{
"0": "Roles",
"1": "app.module.ts",
"2": "AdminTransactionFilterDto",
"3": "productService.ts",
"4": "PetProfile.tsx",
"5": "CmsController",
"6": "TicketsService",
"7": "SmsSettingsPage.tsx",
"8": "SettingsController",
"9": "devDependencies",
"10": "CreateReviewDto",
"11": "WikiController",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "toPersian",
"15": "src/services/api.ts",
"16": "DoctorQueryDto",
"17": "schema.ts",
"18": "JwtAuthGuard",
"19": "admin.controller.ts",
"20": "CreateVideoDto",
"21": ".getSmsConfig",
"22": "BlogsService",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
"26": "ADM-001",
"27": "DB-001",
"28": "TS-001",
"29": "TEST-001",
"30": "DEVOPS-001",
"31": "DOC-001",
"32": "adminRoutes.tsx",
"33": "WholesaleApplyDto",
"34": "B2BService",
"35": "ContactService",
"36": "FaqService",
"37": "راهنمای تست سیستم (Software Testing)",
"38": "Button",
"39": "CategoriesController",
"40": "MediaController",
"41": "What You Must Do When Invoked",
"42": "SslController",
"43": "BannersService",
"44": "TestimonialsService",
"45": "What You Must Do When Invoked",
"46": "20260526145407_init/migration.sql",
"47": "IngredientsService",
"48": "auth.controller.ts",
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsController",
"53": "SmartAdvisorService",
"54": "Button.tsx",
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "UserDashboard.tsx",
"59": "compilerOptions",
"60": "CreateUserDto",
"61": "ProductPage.tsx",
"62": "WikiService",
"63": "dependencies",
"64": "compilerOptions",
"65": "admin.service.ts",
"66": "AdminQueryDto",
"67": "ReportsController",
"68": "useSettingsStore",
"69": "Required Review Group Closures",
"70": "compilerOptions",
"71": "getPageMetadata",
"72": "Operational Rules & Boundaries",
"73": "Operational Rules & Boundaries",
"74": "WikiController",
"75": "PetsController",
"76": "ProductsService",
"77": "seo.module.ts",
"78": "rss.xml/route.ts",
"79": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"80": "Operational Rules & Boundaries",
"81": "Operational Rules & Boundaries",
"82": "scripts",
"83": "dependencies",
"84": "Role & Core Objective",
"85": "auth.module.ts",
"86": "zibal.service.ts",
"87": "dependencies",
"88": "payment.controller.ts",
"89": "seed-products.ts",
"90": "OrderService",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "ArchivePage.tsx",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "wiki/[slug]/page.tsx",
"96": "compilerOptions",
"97": "PaymentService",
"98": "scripts",
"99": "BlogsController",
"100": "Deep Audit Summary Report",
"101": "Operational Rules & Boundaries",
"102": "jest",
"103": "Comprehensive Change Log",
"104": "Products.tsx",
"105": "Operational Rules & Boundaries",
"106": "cms.controller.ts",
"107": "PaginationDto",
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
"110": "Operational Rules & Boundaries",
"111": "Operational Rules & Boundaries",
"112": "Operational Rules & Boundaries",
"113": "PetsController",
"114": "AppService",
"115": "Spinner.tsx",
"116": "Vazirmatn Changelog",
"117": "Vazirmatn Font فونت وزیرمتن",
"118": "Operational Rules & Boundaries",
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
"122": "AdminController",
"123": "UsersService",
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
"128": "AdminService",
"129": "Reports.tsx",
"130": "Sahel-Font",
"131": "Role & Core Objective",
"132": "orchestrate.py",
"133": "backend/package.json",
"134": "blog/page.tsx",
"135": "graphify reference: extra exports and benchmark",
"136": "Phase 2 Final Quality Gate Summary Report",
"137": "Task Modifications Log",
"138": "Install",
"139": "layout.tsx",
"140": "ErrorBoundary",
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "SafeImage.tsx",
"145": "AuthService",
"146": "System Discovery",
"147": "HomeController",
"148": "track/page.tsx",
"149": "trust-seals/page.tsx",
"150": "Product Requirement Document (PRD)",
"151": "menu.module.ts",
"152": "auth.service.ts",
"153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts",
"156": "manual-test-scenarios.md",
"157": "ErrorPages.tsx",
"158": "media/[...path]/route.ts",
"159": "with-vpn.sh",
"160": "Architecture Specification",
"161": "Project Health Audit Report",
"162": "nest-cli.json",
"163": "graphify reference: query, path, explain",
"164": "Open Questions",
"165": "Final Phase 2 Audit Closure Report",
"166": "open-browsers.js",
"167": "📝 Active Agent Working Scratchpad",
"168": "🔍 Code Health Audit Review (01_auditor)",
"169": "paginated-response.schema.ts",
"170": "Vazirmatn Font README",
"171": "Omitted File Inspection Report",
"172": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"173": "Phase 3 Audit Traceability Matrix",
"174": "rebuild_honest_ledger.js",
"175": "validate_evidence_grade.js",
"176": "uploads/[...path]/route.ts",
"177": "app/page.tsx",
"178": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
"179": "app.e2e-spec.js",
"180": "API Contract Specification",
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController",
"185": "RouteErrorBoundary",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
"188": "update-blog.dto.ts",
"189": "update-home.dto.ts",
"190": "update-wiki.dto.ts",
"191": "graphify reference: add a URL and watch a folder",
"192": "graphify reference: commit hook and native CLAUDE.md integration",
"193": "graphify reference: incremental update and cluster-only",
"194": "Raw Finding Verification & Disposition Report",
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"197": "userStore.ts",
"198": "@tailwindcss/postcss",
"199": "SmsLogQueryDto",
"200": "application/README.md",
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "zibal-ebank.service.ts",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
"208": "graphify reference: GitHub clone and cross-repo merge",
"209": "graphify reference: transcribe video and audio",
"210": "Compiler Diagnostic Dispositions",
"211": "Master Task Backlog (Phase 3.3)",
"212": "build_manifest.js",
"213": "generate_classification.js",
"214": "generate_evidence.js",
"215": "generate_ledger.js",
"216": "generate_manifest.js",
"217": "sync_honest_manifest.js",
"218": "sync_manifest.js",
"219": "FormField.tsx",
"220": "jest",
"221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json",
"223": "tailwindcss",
"224": "class-transformer",
"225": "next.config.ts",
"226": "Shabnam Font README",
"227": "AGENTS.md",
"228": "rules/graphify.md",
"229": ".agents/workflows/graphify.md",
"230": "instructions.md",
"231": "@nestjs/schematics",
"232": "prisma",
"233": "Reviews.tsx",
"234": "source-map-support",
"235": "ts-loader",
"236": "ts-node",
"237": "tsconfig-paths",
"238": "@vitejs/plugin-react",
"239": "@types/bcrypt",
"240": "supertest",
"241": "blog.entity.ts",
"242": "home.entity.ts",
"243": "wiki.entity.ts",
"244": "User Profile Photo",
"245": "CLAUDE.md",
"246": ".claude/CLAUDE.md",
"247": "extraction-spec.md",
"248": "Products Table",
"249": "Users Table",
"250": "Architectural Audit Findings",
"251": "Cross Boundary Dependencies & Backend Architecture Specification",
"252": "Next.js Agent Rules & Brand Guidelines",
"253": "robots.ts",
"254": "application/eslint.config.mjs",
"255": "postcss.config.mjs",
"256": "vitest.setup.ts",
"257": "backup_db.sh",
"258": "start.sh",
"259": "reviews/README.md",
"260": "backend/eslint.config.mjs",
"261": "User Login API",
"262": "User Logout API",
"263": "generate-openapi.d.ts",
"264": "@types/compression",
"265": "helmet",
"266": "@types/express",
"267": "@types/jest",
"268": "@types/multer",
"269": "eslint-plugin-react-hooks",
"270": "app-audit-verification.e2e-spec.d.ts",
"271": "app.e2e-spec.d.ts",
"272": "Canina Pharma GmbH",
"273": "Pets Table",
"274": "Canina Iran Project Introduction",
"275": "Developer Standards and Architecture",
"276": "Frontend & Admin Architecture Route Map Specification",
"277": "Project Backlog and Tasks",
"278": "eslint.config.js",
"279": "postcss.config.js",
"280": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"281": "shabnam-font-v5.0.1/CHANGELOG.md",
"282": "tailwind.config.js",
"283": "vite.config.ts",
"284": "application/CLAUDE.md",
"285": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"286": "Sahel Font Sample",
"287": "Shabnam Font Changelog",
"288": "Vazirmatn Changelog",
"289": "vitest.config.ts",
"290": "Sahel Font Variable Sample",
"291": "Shabnam Font Sample",
"292": "Production Docker Compose",
"293": "Staging Docker Compose",
"294": "ZibalService",
"295": "js-yaml",
"296": "@nestjs/core",
"297": "ZibalEBankService",
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
"300": "typescript",
"301": "@nestjs/jwt",
"302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
"305": "reflect-metadata",
"306": "@eslint/js",
"307": "swagger-ui-express",
"308": "eslint-config-prettier",
"309": "axios",
"310": "tailwindcss",
"311": "@eslint/eslintrc",
"312": "@types/passport-jwt",
"313": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"314": "eslint-plugin-prettier",
"315": "typescript",
"316": "globals",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx",
"319": "@nestjs/cli",
"320": "@nestjs/testing",
"321": "orders/page.tsx",
"322": "pets/page.tsx",
"323": "prettier",
"324": "eslint",
"325": "@types/react-dom",
"326": "@types/js-yaml",
"327": "eslint-plugin-react-refresh",
"328": "@types/supertest",
"329": "typescript-eslint",
"330": "@nestjs/swagger",
"331": "tailwindcss"
}

View File

@ -0,0 +1 @@
{"output_tokens": 7105}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,32 @@
# Graph Report - canina (2026-09-02) # Graph Report - canina (2026-09-05)
## Corpus Check ## Corpus Check
- 599 files · ~1,084,689 words - 601 files · ~1,114,020 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 4208 nodes · 7643 edges · 332 communities (212 shown, 120 thin omitted) - 4234 nodes · 7717 edges · 310 communities (215 shown, 95 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 288 edges (avg confidence: 0.79) - Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 298 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `ceaab10f` - Built from commit: `e15c25da`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation) ## Community Hubs (Navigation)
- Roles - Roles
- app.module.ts - app.module.ts
- AdminTransactionFilterDto - payment.service.ts
- productService.ts - productService.ts
- PetProfile.tsx - UserDashboard.tsx
- CmsController - CmsController
- TicketsService - tickets.controller.ts
- SmsSettingsPage.tsx - SmsSettingsPage.tsx
- SettingsController - SmsService
- devDependencies - devDependencies
- CreateReviewDto - ReviewsService
- WikiController - ConfirmModal.tsx
- index.ts - index.ts
- app-audit-verification.e2e-spec.js - app-audit-verification.e2e-spec.js
- toPersian - toPersian
@ -36,7 +36,8 @@
- JwtAuthGuard - JwtAuthGuard
- admin.controller.ts - admin.controller.ts
- CreateVideoDto - CreateVideoDto
- BlogsService - auth.service.ts
- ProductDto
- MenuService - MenuService
- BE-001 - BE-001
- FE-001 - FE-001
@ -62,26 +63,26 @@
- What You Must Do When Invoked - What You Must Do When Invoked
- 20260526145407_init/migration.sql - 20260526145407_init/migration.sql
- IngredientsService - IngredientsService
- auth.controller.ts - AuthController
- devDependencies - devDependencies
- devDependencies - devDependencies
- BlogsController - BlogsController
- PrescriptionsController - PrescriptionsService
- SmartAdvisorService - SmartAdvisorService
- Button.tsx - Button.tsx
- UITexts.tsx - UITexts.tsx
- Orders.tsx - Orders.tsx
- Role & Core Objective - Role & Core Objective
- UserDashboard.tsx - AuthService
- compilerOptions - compilerOptions
- CreateUserDto - CreateUserDto
- ProductPage.tsx - ProductPage.tsx
- WikiService - torob.controller.ts
- dependencies - dependencies
- compilerOptions - compilerOptions
- admin.service.ts - admin.service.ts
- AdminQueryDto - AdminQueryDto
- ReportsController - admin.module.ts
- useSettingsStore - useSettingsStore
- Required Review Group Closures - Required Review Group Closures
- compilerOptions - compilerOptions
@ -99,17 +100,17 @@
- scripts - scripts
- dependencies - dependencies
- Role & Core Objective - Role & Core Objective
- auth.module.ts - RegisterDto
- zibal.service.ts - zibal.service.ts
- dependencies - dependencies
- payment.controller.ts - CreateEBankCheckoutDto
- seed-products.ts - seed-products.ts
- OrderService - useCartStore
- Reconciled Audit Roles & Assignments - Reconciled Audit Roles & Assignments
- OrdersService - OrdersService
- ArchivePage.tsx - ArchivePage.tsx
- Phase 3.1 — Human Review Preparation and Master Backlog Critique - Phase 3.1 — Human Review Preparation and Master Backlog Critique
- wiki/[slug]/page.tsx - getSeoConfig
- compilerOptions - compilerOptions
- PaymentService - PaymentService
- scripts - scripts
@ -120,7 +121,7 @@
- Comprehensive Change Log - Comprehensive Change Log
- Products.tsx - Products.tsx
- Operational Rules & Boundaries - Operational Rules & Boundaries
- cms.controller.ts - InitiatePaymentDto
- PaginationDto - PaginationDto
- PrismaService - PrismaService
- 1. Summary of Integrity Repairs Performed - 1. Summary of Integrity Repairs Performed
@ -136,13 +137,13 @@
- compilerOptions - compilerOptions
- compilerOptions - compilerOptions
- backend/README.md - backend/README.md
- AdminController - AdminService
- UsersService - UsersService
- Repository Map - Repository Map
- validate_integrity.js - validate_integrity.js
- admin-panel/package.json - admin-panel/package.json
- Sahel-Font - Sahel-Font
- AdminService - Body
- Reports.tsx - Reports.tsx
- Sahel-Font - Sahel-Font
- Role & Core Objective - Role & Core Objective
@ -158,15 +159,15 @@
- application/package.json - application/package.json
- start-dev.js - start-dev.js
- generate-openapi.js - generate-openapi.js
- SafeImage.tsx - PodcastPlayerModal.tsx
- AuthService - AdminLoginDto
- System Discovery - System Discovery
- HomeController - HomeController
- track/page.tsx - VerifyOtpDto
- trust-seals/page.tsx - wiki/[slug]/page.tsx
- Product Requirement Document (PRD) - Product Requirement Document (PRD)
- menu.module.ts - @types/node
- auth.service.ts - RevalidationService
- exclude - exclude
- Baseline Command Plan & Reconciled Command History - Baseline Command Plan & Reconciled Command History
- seo-backfill.ts - seo-backfill.ts
@ -197,8 +198,8 @@
- ⚙️ Backend Technical Review (05_dev_backend) - ⚙️ Backend Technical Review (05_dev_backend)
- 🎨 Frontend & Admin Panel Technical Review (06_dev_frontend) - 🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)
- 🚀 SEO & Content Strategy Review (12_seo_content) - 🚀 SEO & Content Strategy Review (12_seo_content)
- MetricsController - AppModule
- RouteErrorBoundary - eslint-config-next
- seed-ui-texts.ts - seed-ui-texts.ts
- seed-wiki.ts - seed-wiki.ts
- update-blog.dto.ts - update-blog.dto.ts
@ -217,7 +218,6 @@
- deploy.sh - deploy.sh
- 🔒 Security & Performance Review (09_devops_security) - 🔒 Security & Performance Review (09_devops_security)
- 👁️ UX & Persona Interface Review (08_visual_qa) - 👁️ UX & Persona Interface Review (08_visual_qa)
- zibal-ebank.service.ts
- prisma/scientificTerms.ts - prisma/scientificTerms.ts
- seed-blogs.ts - seed-blogs.ts
- seed-custom.ts - seed-custom.ts
@ -233,11 +233,9 @@
- sync_honest_manifest.js - sync_honest_manifest.js
- sync_manifest.js - sync_manifest.js
- FormField.tsx - FormField.tsx
- jest
- Textarea.tsx - Textarea.tsx
- admin-panel/tsconfig.json - admin-panel/tsconfig.json
- tailwindcss - tailwindcss
- class-transformer
- next.config.ts - next.config.ts
- Shabnam Font README - Shabnam Font README
- AGENTS.md - AGENTS.md
@ -246,7 +244,6 @@
- instructions.md - instructions.md
- @nestjs/schematics - @nestjs/schematics
- prisma - prisma
- Reviews.tsx
- source-map-support - source-map-support
- ts-loader - ts-loader
- ts-node - ts-node
@ -274,7 +271,6 @@
- User Login API - User Login API
- User Logout API - User Logout API
- @types/compression - @types/compression
- helmet
- @types/express - @types/express
- @types/jest - @types/jest
- @types/multer - @types/multer
@ -293,21 +289,12 @@
- Production Docker Compose - Production Docker Compose
- Staging Docker Compose - Staging Docker Compose
- ZibalService - ZibalService
- js-yaml
- @nestjs/core
- ZibalEBankService - ZibalEBankService
- .initiateOrderPayment - .initiateOrderPayment
- @tailwindcss/postcss - @tailwindcss/postcss
- typescript - typescript
- @nestjs/jwt
- typescript-eslint - typescript-eslint
- @nestjs/throttler
- passport
- reflect-metadata
- @eslint/js - @eslint/js
- swagger-ui-express
- eslint-config-prettier
- @eslint/eslintrc
- @types/passport-jwt - @types/passport-jwt
- 20260526160916_add_ui_texts_and_scientific_terms/migration.sql - 20260526160916_add_ui_texts_and_scientific_terms/migration.sql
- eslint-plugin-prettier - eslint-plugin-prettier
@ -315,24 +302,16 @@
- globals - globals
- revalidate/route.ts - revalidate/route.ts
- MaskableField.tsx - MaskableField.tsx
- @nestjs/cli
- @nestjs/testing
- prettier
- eslint
- @types/react-dom - @types/react-dom
- @types/js-yaml
- eslint-plugin-react-refresh - eslint-plugin-react-refresh
- @types/supertest
- typescript-eslint - typescript-eslint
- @nestjs/swagger
- tailwindcss
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges 1. `Roles()` - 108 edges
2. `PrismaService` - 89 edges 2. `PrismaService` - 89 edges
3. `useSettingsStore` - 61 edges 3. `useSettingsStore` - 61 edges
4. `api` - 44 edges 4. `SmsService` - 51 edges
5. `SmsService` - 43 edges 5. `api` - 44 edges
6. `PaginationDto` - 41 edges 6. `PaginationDto` - 41 edges
7. `AdminService` - 40 edges 7. `AdminService` - 40 edges
8. `AdminController` - 39 edges 8. `AdminController` - 39 edges
@ -352,75 +331,75 @@
backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts
## Import Cycles ## Import Cycles
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts` - 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts` - 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (332 total, 120 thin omitted) ## Communities (310 total, 95 thin omitted)
### Community 0 - "Roles" ### Community 0 - "Roles"
Cohesion: 0.24 Cohesion: 0.24
Nodes (13): Roles(), PaymentController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get (+5 more) Nodes (13): Roles(), PaymentController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get (+5 more)
### Community 1 - "app.module.ts" ### Community 1 - "app.module.ts"
Cohesion: 0.06 Cohesion: 0.07
Nodes (39): B2BModule, Module, BannersModule, Module, CmsModule, Module, RevalidationModule, Global (+31 more) Nodes (36): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+28 more)
### Community 2 - "AdminTransactionFilterDto" ### Community 2 - "payment.service.ts"
Cohesion: 0.22 Cohesion: 0.20
Nodes (7): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type Nodes (8): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type, ClientMetadata
### Community 3 - "productService.ts" ### Community 3 - "productService.ts"
Cohesion: 0.06 Cohesion: 0.05
Nodes (39): buildTorobProductSpec(), buildTorobProductTitle(), dynamic, GET(), revalidate, dynamic, GET(), revalidate (+31 more) Nodes (41): buildTorobProductSpec(), buildTorobProductTitle(), dynamic, GET(), revalidate, dynamic, GET(), revalidate (+33 more)
### Community 4 - "PetProfile.tsx" ### Community 4 - "UserDashboard.tsx"
Cohesion: 0.12 Cohesion: 0.11
Nodes (18): metadata, FeaturedProducts(), ProductCard(), OrderSuccess(), PetProfile(), SearchResultsPage(), CURRENT_SYMPTOMS, MEDICAL_HISTORIES (+10 more) Nodes (20): metadata, OrderDetailsModal(), PetProfile(), SearchResultsPage(), CURRENT_SYMPTOMS, MEDICAL_HISTORIES, SmartAdvisor(), SmartAdvisorProps (+12 more)
### Community 5 - "CmsController" ### Community 5 - "CmsController"
Cohesion: 0.10 Cohesion: 0.09
Nodes (14): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
### Community 6 - "TicketsService" ### Community 6 - "tickets.controller.ts"
Cohesion: 0.09 Cohesion: 0.09
Nodes (29): AdminUpdateTicketDto, CreateTicketDto, ReplyTicketDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString (+21 more) Nodes (29): AdminUpdateTicketDto, CreateTicketDto, ReplyTicketDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString (+21 more)
### Community 7 - "SmsSettingsPage.tsx" ### Community 7 - "SmsSettingsPage.tsx"
Cohesion: 0.29 Cohesion: 0.20
Nodes (7): PatternItem, SmsConfigState, SmsLogItem, SmsLogStats, SmsSettingsPage(), toPersianDigits(), SmsSettingsPage Nodes (10): PatternItem, SmsConfigState, SmsEventDefinition, SmsEventVariable, SmsLogItem, SmsLogStats, SmsRule, SmsSettingsPage() (+2 more)
### Community 8 - "SettingsController" ### Community 8 - "SmsService"
Cohesion: 0.10 Cohesion: 0.06
Nodes (17): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+9 more) Nodes (20): SmsEventDefinition, SmsService, Injectable, SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags (+12 more)
### Community 9 - "devDependencies" ### Community 9 - "devDependencies"
Cohesion: 0.22 Cohesion: 0.08
Nodes (9): devDependencies, ts-jest, @types/bcryptjs, @types/node, typescript, @types/node, typescript, ts-jest (+1 more) Nodes (25): devDependencies, eslint, eslint-config-prettier, @eslint/eslintrc, jest, @nestjs/cli, @nestjs/testing, prettier (+17 more)
### Community 10 - "CreateReviewDto" ### Community 10 - "ReviewsService"
Cohesion: 0.07 Cohesion: 0.09
Nodes (30): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, ApiProperty (+22 more) Nodes (22): ApiProperty, IsIn, IsOptional, IsString, UpdateReviewDto, ReviewsController, ApiBearerAuth, ApiOperation (+14 more)
### Community 11 - "WikiController" ### Community 11 - "ConfirmModal.tsx"
Cohesion: 0.21 Cohesion: 0.10
Nodes (9): ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, Param, Query (+1 more) Nodes (16): ConfirmModal(), ConfirmModalProps, Pagination(), PaginationProps, Category, Pet, DoctorOption, Video (+8 more)
### Community 12 - "index.ts" ### Community 12 - "index.ts"
Cohesion: 0.06 Cohesion: 0.06
Nodes (24): backend, frontend, playwright.config.ts, @playwright/test, tests/**/*.ts, authFile, test, compilerOptions (+16 more) Nodes (24): backend, frontend, playwright.config.ts, @playwright/test, tests/**/*.ts, authFile, test, compilerOptions (+16 more)
### Community 13 - "app-audit-verification.e2e-spec.js" ### Community 13 - "app-audit-verification.e2e-spec.js"
Cohesion: 0.06 Cohesion: 0.07
Nodes (28): AppModule, Module, EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter (+20 more) Nodes (26): EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter, Catch, PrismaExceptionFilter (+18 more)
### Community 14 - "toPersian" ### Community 14 - "toPersian"
Cohesion: 0.17 Cohesion: 0.17
Nodes (20): HomeClient(), VerifyContent(), AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), CheckoutPage() (+12 more) Nodes (19): VerifyContent(), AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), BackButton(), BackButtonProps (+11 more)
### Community 15 - "src/services/api.ts" ### Community 15 - "src/services/api.ts"
Cohesion: 0.06 Cohesion: 0.06
Nodes (36): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+28 more) Nodes (38): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+30 more)
### Community 16 - "DoctorQueryDto" ### Community 16 - "DoctorQueryDto"
Cohesion: 0.09 Cohesion: 0.09
@ -431,16 +410,24 @@ Cohesion: 0.14
Nodes (18): B2BPage(), generateMetadata(), ContactPage(), generateMetadata(), generateMetadata(), getInitialVideos(), Videos(), ContactFormClient() (+10 more) Nodes (18): B2BPage(), generateMetadata(), ContactPage(), generateMetadata(), generateMetadata(), getInitialVideos(), Videos(), ContactFormClient() (+10 more)
### Community 18 - "JwtAuthGuard" ### Community 18 - "JwtAuthGuard"
Cohesion: 0.19 Cohesion: 0.10
Nodes (8): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload, ScientificTermData Nodes (19): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, ApiPropertyOptional, IsOptional (+11 more)
### Community 19 - "admin.controller.ts" ### Community 19 - "admin.controller.ts"
Cohesion: 0.35 Cohesion: 0.29
Nodes (12): ApplyGlobalMarginsDto, BulkPriceAdjustmentDto, ApiProperty, ApiPropertyOptional, IsArray, IsBoolean, IsIn, IsNumber (+4 more) Nodes (12): ApplyGlobalMarginsDto, BulkPriceAdjustmentDto, ApiProperty, ApiPropertyOptional, IsArray, IsBoolean, IsIn, IsNumber (+4 more)
### Community 20 - "CreateVideoDto" ### Community 20 - "CreateVideoDto"
Cohesion: 0.07 Cohesion: 0.07
Nodes (31): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+23 more) Nodes (32): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+24 more)
### Community 21 - "auth.service.ts"
Cohesion: 0.14
Nodes (13): AdminLoginInput, LoginInput, RegisterInput, LoginDto, ApiProperty, IsNotEmpty, IsString, MinLength (+5 more)
### Community 22 - "ProductDto"
Cohesion: 0.16
Nodes (7): ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional, IsString
### Community 23 - "MenuService" ### Community 23 - "MenuService"
Cohesion: 0.12 Cohesion: 0.12
@ -480,7 +467,7 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
### Community 32 - "adminRoutes.tsx" ### Community 32 - "adminRoutes.tsx"
Cohesion: 0.07 Cohesion: 0.07
Nodes (21): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, SslStatus, Ticket, TicketMessage (+13 more) Nodes (16): App(), Props, RouteErrorBoundary, State, HeroBanner, VetTestimonial, SslStatus, AdminRouteConfig (+8 more)
### Community 33 - "WholesaleApplyDto" ### Community 33 - "WholesaleApplyDto"
Cohesion: 0.10 Cohesion: 0.10
@ -495,8 +482,8 @@ Cohesion: 0.13
Nodes (12): ContactController, Body, Controller, Get, Param, Post, Put, Query (+4 more) Nodes (12): ContactController, Body, Controller, Get, Param, Post, Put, Query (+4 more)
### Community 36 - "FaqService" ### Community 36 - "FaqService"
Cohesion: 0.14 Cohesion: 0.12
Nodes (14): FaqController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (16): FaqController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
### Community 37 - "راهنمای تست سیستم (Software Testing)" ### Community 37 - "راهنمای تست سیستم (Software Testing)"
Cohesion: 0.07 Cohesion: 0.07
@ -519,8 +506,8 @@ Cohesion: 0.07
Nodes (26): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+18 more) Nodes (26): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+18 more)
### Community 42 - "SslController" ### Community 42 - "SslController"
Cohesion: 0.11 Cohesion: 0.13
Nodes (15): SslController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Post (+7 more) Nodes (14): SslController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Post (+6 more)
### Community 43 - "BannersService" ### Community 43 - "BannersService"
Cohesion: 0.13 Cohesion: 0.13
@ -542,9 +529,9 @@ Nodes (14): "coupons", "health_logs", "order_items", "orders", "pet_medical_cond
Cohesion: 0.13 Cohesion: 0.13
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 48 - "auth.controller.ts" ### Community 48 - "AuthController"
Cohesion: 0.06 Cohesion: 0.25
Nodes (43): AuthController, ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+35 more) Nodes (13): AuthController, ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+5 more)
### Community 49 - "devDependencies" ### Community 49 - "devDependencies"
Cohesion: 0.11 Cohesion: 0.11
@ -552,27 +539,27 @@ Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, glo
### Community 50 - "devDependencies" ### Community 50 - "devDependencies"
Cohesion: 0.12 Cohesion: 0.12
Nodes (17): eslint-config-next, devDependencies, eslint, eslint-config-next, jsdom, @testing-library/jest-dom, @testing-library/react, @types/node (+9 more) Nodes (17): devDependencies, eslint, jsdom, tailwindcss, @testing-library/jest-dom, @testing-library/react, @types/node, @types/react (+9 more)
### Community 51 - "BlogsController" ### Community 51 - "BlogsController"
Cohesion: 0.07 Cohesion: 0.07
Nodes (18): BlogsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+10 more) Nodes (18): BlogsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+10 more)
### Community 52 - "PrescriptionsController" ### Community 52 - "PrescriptionsService"
Cohesion: 0.16 Cohesion: 0.14
Nodes (12): PrescriptionsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+4 more) Nodes (14): PrescriptionsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+6 more)
### Community 53 - "SmartAdvisorService" ### Community 53 - "SmartAdvisorService"
Cohesion: 0.13 Cohesion: 0.13
Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 54 - "Button.tsx" ### Community 54 - "Button.tsx"
Cohesion: 0.10 Cohesion: 0.07
Nodes (16): ButtonProps, ButtonSize, ButtonVariant, ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, FAQ (+8 more) Nodes (24): ButtonProps, ButtonSize, ButtonVariant, maxWidthClasses, Modal(), ModalProps, ContactInfoItem, ContactSubmission (+16 more)
### Community 55 - "UITexts.tsx" ### Community 55 - "UITexts.tsx"
Cohesion: 0.07 Cohesion: 0.08
Nodes (26): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+18 more) Nodes (23): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+15 more)
### Community 56 - "Orders.tsx" ### Community 56 - "Orders.tsx"
Cohesion: 0.11 Cohesion: 0.11
@ -582,45 +569,49 @@ Nodes (15): Badge(), BadgeProps, BadgeVariant, variantStyles, Skeleton(), Monito
Cohesion: 0.09 Cohesion: 0.09
Nodes (22): CREATE MODE — Normal Operation, Expected JSON Output Schema, File Scope Boundary, Forbidden Actions, MODE 1: REVIEW (called during Review Phase), MODE 2: CONTENT CREATION (standalone task from backlog), Operating Modes, Operational Rules & Boundaries (+14 more) Nodes (22): CREATE MODE — Normal Operation, Expected JSON Output Schema, File Scope Boundary, Forbidden Actions, MODE 1: REVIEW (called during Review Phase), MODE 2: CONTENT CREATION (standalone task from backlog), Operating Modes, Operational Rules & Boundaries (+14 more)
### Community 58 - "UserDashboard.tsx" ### Community 58 - "AuthService"
Cohesion: 0.12 Cohesion: 0.19
Nodes (11): DeleteConfirmModal(), DeleteConfirmModalProps, OrderDetailsModal(), OrderRowSkeleton(), PetProfileSkeleton(), Skeleton(), SkeletonProps, CreateTicketPayload (+3 more) Nodes (3): AuthService, Injectable, normalizeMobile()
### Community 59 - "compilerOptions" ### Community 59 - "compilerOptions"
Cohesion: 0.06 Cohesion: 0.06
Nodes (30): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration, emitDecoratorMetadata, esModuleInterop, experimentalDecorators, forceConsistentCasingInFileNames (+22 more) Nodes (30): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration, emitDecoratorMetadata, esModuleInterop, experimentalDecorators, forceConsistentCasingInFileNames (+22 more)
### Community 60 - "CreateUserDto" ### Community 60 - "CreateUserDto"
Cohesion: 0.23 Cohesion: 0.21
Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more) Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more)
### Community 61 - "ProductPage.tsx" ### Community 61 - "ProductPage.tsx"
Cohesion: 0.10 Cohesion: 0.09
Nodes (19): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_MAP, isVideoUrl(), ProductImageZoomModal, ProductPage(), ProductReviews (+11 more) Nodes (20): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_MAP, isVideoUrl(), ProductImageZoomModal, ProductPage(), ProductReviews (+12 more)
### Community 62 - "torob.controller.ts"
Cohesion: 0.22
Nodes (8): buildTorobProductSpec(), buildTorobProductTitle(), TorobController, ApiOperation, ApiTags, Controller, Get, Query
### Community 63 - "dependencies" ### Community 63 - "dependencies"
Cohesion: 0.09 Cohesion: 0.05
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more) Nodes (43): dependencies, bcrypt, bcryptjs, class-transformer, class-validator, compression, helmet, ioredis (+35 more)
### Community 64 - "compilerOptions" ### Community 64 - "compilerOptions"
Cohesion: 0.10 Cohesion: 0.10
Nodes (20): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, lib, module, moduleDetection, moduleResolution, noEmit (+12 more) Nodes (20): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, lib, module, moduleDetection, moduleResolution, noEmit (+12 more)
### Community 65 - "admin.service.ts" ### Community 65 - "admin.service.ts"
Cohesion: 0.14 Cohesion: 0.33
Nodes (15): CouponTargetInput, PaginationQuery, ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional (+7 more) Nodes (8): CouponTargetInput, PaginationQuery, applyRounding(), calculateSellingPrice(), DEFAULT_PRICING_SETTINGS, PricingSettings, RoundingMode, CANONICAL_ALIASES
### Community 66 - "AdminQueryDto" ### Community 66 - "AdminQueryDto"
Cohesion: 0.13 Cohesion: 0.18
Nodes (8): ApiQuery, Get, Query, AdminProductQueryDto, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString Nodes (7): ApiQuery, Query, AdminProductQueryDto, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
### Community 67 - "ReportsController" ### Community 67 - "admin.module.ts"
Cohesion: 0.14 Cohesion: 0.08
Nodes (11): ReportsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Get, Query (+3 more) Nodes (17): AdminModule, Module, ReportsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller (+9 more)
### Community 68 - "useSettingsStore" ### Community 68 - "useSettingsStore"
Cohesion: 0.08 Cohesion: 0.08
Nodes (29): HomeClientProps, B2BLandingClient(), BlogPost, BlogPreviewSection(), BrandLogo(), BrandLogoProps, ContactInfoItem, FAQItem (+21 more) Nodes (36): HomeClient(), HomeClientProps, B2BLandingClient(), BlogPostClientProps, BlogPost, BlogPreviewSection(), ContactInfoItem, EnamadBadge() (+28 more)
### Community 69 - "Required Review Group Closures" ### Community 69 - "Required Review Group Closures"
Cohesion: 0.10 Cohesion: 0.10
@ -631,8 +622,8 @@ Cohesion: 0.08
Nodes (23): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+15 more) Nodes (23): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+15 more)
### Community 71 - "getPageMetadata" ### Community 71 - "getPageMetadata"
Cohesion: 0.09 Cohesion: 0.08
Nodes (14): generateMetadata(), CatalogClient(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+6 more) Nodes (16): generateMetadata(), CatalogClient(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+8 more)
### Community 72 - "Operational Rules & Boundaries" ### Community 72 - "Operational Rules & Boundaries"
Cohesion: 0.11 Cohesion: 0.11
@ -643,7 +634,7 @@ Cohesion: 0.11
Nodes (18): 1. Framework-Aware Analysis, 2. DOM & Semantic Structure Analysis, 3. Accessibility Compliance, 4. Responsive Layout Verification, 5. Fallback Inspection Mode, 6. Defect Routing Protocol, 7. Forbidden Actions, Expected JSON Output Schema (+10 more) Nodes (18): 1. Framework-Aware Analysis, 2. DOM & Semantic Structure Analysis, 3. Accessibility Compliance, 4. Responsive Layout Verification, 5. Fallback Inspection Mode, 6. Defect Routing Protocol, 7. Forbidden Actions, Expected JSON Output Schema (+10 more)
### Community 74 - "WikiController" ### Community 74 - "WikiController"
Cohesion: 0.14 Cohesion: 0.13
Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 75 - "PetsController" ### Community 75 - "PetsController"
@ -651,8 +642,8 @@ Cohesion: 0.05
Nodes (47): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+39 more) Nodes (47): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+39 more)
### Community 76 - "ProductsService" ### Community 76 - "ProductsService"
Cohesion: 0.07 Cohesion: 0.10
Nodes (27): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+19 more) Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
### Community 77 - "seo.module.ts" ### Community 77 - "seo.module.ts"
Cohesion: 0.16 Cohesion: 0.16
@ -686,9 +677,9 @@ Nodes (21): dependencies, axios, lucide-react, react, react-dom, react-hot-toast
Cohesion: 0.12 Cohesion: 0.12
Nodes (16): 1. Active Secret Scanning (All Modified Files), 2. Docker Container Verification (if Dockerfile exists), 3. CI/CD Basic Check (if `.github/workflows/` exists), 4. Failure Routing Protocol, 5. Forbidden Actions, ENFORCE MODE — Normal Operation, Expected JSON Output Schema, Operational Rules & Boundaries (+8 more) Nodes (16): 1. Active Secret Scanning (All Modified Files), 2. Docker Container Verification (if Dockerfile exists), 3. CI/CD Basic Check (if `.github/workflows/` exists), 4. Failure Routing Protocol, 5. Forbidden Actions, ENFORCE MODE — Normal Operation, Expected JSON Output Schema, Operational Rules & Boundaries (+8 more)
### Community 85 - "auth.module.ts" ### Community 85 - "RegisterDto"
Cohesion: 0.17 Cohesion: 0.22
Nodes (10): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+2 more) Nodes (8): RegisterDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsOptional, IsString, MinLength
### Community 86 - "zibal.service.ts" ### Community 86 - "zibal.service.ts"
Cohesion: 0.16 Cohesion: 0.16
@ -698,33 +689,37 @@ Nodes (9): GatewayHealthResult, IPaymentGateway, PaymentInquiryResult, PaymentRe
Cohesion: 0.11 Cohesion: 0.11
Nodes (19): dependencies, axios, lucide-react, motion, next, nextjs-toploader, react, react-dom (+11 more) Nodes (19): dependencies, axios, lucide-react, motion, next, nextjs-toploader, react, react-dom (+11 more)
### Community 88 - "payment.controller.ts" ### Community 88 - "CreateEBankCheckoutDto"
Cohesion: 0.12 Cohesion: 0.22
Nodes (19): CreateEBankCheckoutDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsNumber, IsOptional, IsString, Min (+11 more) Nodes (8): CreateEBankCheckoutDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsNumber, IsOptional, IsString, Min
### Community 89 - "seed-products.ts" ### Community 89 - "seed-products.ts"
Cohesion: 0.17 Cohesion: 0.17
Nodes (12): prisma, main(), prisma, determineSuitableFor(), findOrCreateCategory(), getProductImageUrl(), main(), parseSize() (+4 more) Nodes (12): prisma, main(), prisma, determineSuitableFor(), findOrCreateCategory(), getProductImageUrl(), main(), parseSize() (+4 more)
### Community 90 - "useCartStore"
Cohesion: 0.09
Nodes (19): B2BPortal, CartDrawer, B2BPortal(), CartDrawer(), DeleteConfirmModal(), DeleteConfirmModalProps, OrderSuccess(), OrderTracking() (+11 more)
### Community 91 - "Reconciled Audit Roles & Assignments" ### Community 91 - "Reconciled Audit Roles & Assignments"
Cohesion: 0.12 Cohesion: 0.12
Nodes (15): 10. Documentation Engineer, 1. Lead Architect / Orchestrator, 2. React / Vite Storefront Auditor, 3. NestJS Backend Auditor, 4. Admin Features Auditor, 5. Database & Data Integrity Auditor, 6. Security Auditor, 7. TypeScript & Code Quality Auditor (+7 more) Nodes (15): 10. Documentation Engineer, 1. Lead Architect / Orchestrator, 2. React / Vite Storefront Auditor, 3. NestJS Backend Auditor, 4. Admin Features Auditor, 5. Database & Data Integrity Auditor, 6. Security Auditor, 7. TypeScript & Code Quality Auditor (+7 more)
### Community 92 - "OrdersService" ### Community 92 - "OrdersService"
Cohesion: 0.07 Cohesion: 0.06
Nodes (35): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+27 more) Nodes (35): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+27 more)
### Community 93 - "ArchivePage.tsx" ### Community 93 - "ArchivePage.tsx"
Cohesion: 0.13 Cohesion: 0.08
Nodes (15): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, ProductCardSkeleton(), B2BInquiry (+7 more) Nodes (21): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, OrderRowSkeleton(), PetProfileSkeleton() (+13 more)
### Community 94 - "Phase 3.1 — Human Review Preparation and Master Backlog Critique" ### Community 94 - "Phase 3.1 — Human Review Preparation and Master Backlog Critique"
Cohesion: 0.13 Cohesion: 0.13
Nodes (14): 10. Dependency & Wave Adjustments, 11. Acceptance Criteria Refinements, 12. Business and Product Decision Classification, 13. Final Recommended Backlog Summary, 1. Executive Assessment, 2. Findings That Require No Change, 3. Findings Requiring Task Refinements, 4. Tasks Requiring Splitting (+6 more) Nodes (14): 10. Dependency & Wave Adjustments, 11. Acceptance Criteria Refinements, 12. Business and Product Decision Classification, 13. Final Recommended Backlog Summary, 1. Executive Assessment, 2. Findings That Require No Change, 3. Findings Requiring Task Refinements, 4. Tasks Requiring Splitting (+6 more)
### Community 95 - "wiki/[slug]/page.tsx" ### Community 95 - "getSeoConfig"
Cohesion: 0.19 Cohesion: 0.26
Nodes (16): BlogPostPage(), generateMetadata(), getBlog(), revalidate, safeIso(), safeLocalDate(), generateMetadata(), revalidate (+8 more) Nodes (11): BlogPostPage(), generateMetadata(), getBlog(), revalidate, safeIso(), safeLocalDate(), generateMetadata(), revalidate (+3 more)
### Community 96 - "compilerOptions" ### Community 96 - "compilerOptions"
Cohesion: 0.06 Cohesion: 0.06
@ -735,8 +730,8 @@ Cohesion: 0.13
Nodes (15): scripts, build, build:nest, docs:generate, lint, seo:backfill, start, start:debug (+7 more) Nodes (15): scripts, build, build:nest, docs:generate, lint, seo:backfill, start, start:debug (+7 more)
### Community 99 - "BlogsController" ### Community 99 - "BlogsController"
Cohesion: 0.14 Cohesion: 0.18
Nodes (15): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+7 more) Nodes (12): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+4 more)
### Community 100 - "Deep Audit Summary Report" ### Community 100 - "Deep Audit Summary Report"
Cohesion: 0.14 Cohesion: 0.14
@ -755,24 +750,24 @@ Cohesion: 0.15
Nodes (12): 10. `TASK-VERIFY-001`, 1. `TASK-SEC-001`, 2. `TASK-SEC-002`, 3. `TASK-SEC-003`, 4. `TASK-FIN-001`, 5. `TASK-BUILD-001`, 6. `TASK-AUTH-001`, 7. `TASK-FE-001` (+4 more) Nodes (12): 10. `TASK-VERIFY-001`, 1. `TASK-SEC-001`, 2. `TASK-SEC-002`, 3. `TASK-SEC-003`, 4. `TASK-FIN-001`, 5. `TASK-BUILD-001`, 6. `TASK-AUTH-001`, 7. `TASK-FE-001` (+4 more)
### Community 104 - "Products.tsx" ### Community 104 - "Products.tsx"
Cohesion: 0.10 Cohesion: 0.09
Nodes (24): Input, InputProps, formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData (+16 more) Nodes (25): Input, InputProps, formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData (+17 more)
### Community 105 - "Operational Rules & Boundaries" ### Community 105 - "Operational Rules & Boundaries"
Cohesion: 0.17 Cohesion: 0.17
Nodes (11): 1. Detect Test Runner from Tech Stack, 2. Execution Output Capture (Mandatory), 3. Acceptance Criteria Verification, 4. Failure Routing Protocol, 5. Success Routing Protocol, 6. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries (+3 more) Nodes (11): 1. Detect Test Runner from Tech Stack, 2. Execution Output Capture (Mandatory), 3. Acceptance Criteria Verification, 4. Failure Routing Protocol, 5. Success Routing Protocol, 6. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries (+3 more)
### Community 106 - "cms.controller.ts" ### Community 106 - "InitiatePaymentDto"
Cohesion: 0.37 Cohesion: 0.43
Nodes (10): CreateHeroBannerDto, CreateSmartAdvisorRuleDto, CreateVetTestimonialDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNumber, IsOptional (+2 more) Nodes (7): InitiatePaymentDto, InitiateWalletTopupDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
### Community 107 - "PaginationDto" ### Community 107 - "PaginationDto"
Cohesion: 0.12 Cohesion: 0.06
Nodes (15): AdminModule, Module, PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsInt, IsOptional (+7 more) Nodes (27): BlogsModule, Module, BlogFilterDto, BlogsService, Injectable, PaginationDto, SortOrder, ApiPropertyOptional (+19 more)
### Community 108 - "PrismaService" ### Community 108 - "PrismaService"
Cohesion: 0.06 Cohesion: 0.07
Nodes (21): CategoryQuery, PetQuery, WikiQuery, RevalidationService, Injectable, MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions (+13 more) Nodes (23): CategoryQuery, DEFAULT_SMS_RULES, SMS_EVENT_DEFINITIONS, SmsEventVariable, SmsRule, MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions (+15 more)
### Community 109 - "1. Summary of Integrity Repairs Performed" ### Community 109 - "1. Summary of Integrity Repairs Performed"
Cohesion: 0.17 Cohesion: 0.17
@ -791,16 +786,16 @@ Cohesion: 0.18
Nodes (10): 1. Pre-Deployment Checklist, 2. Build Verification, 3. Deployment Strategy (Based on Target), 4. Post-Deployment Health Check, 5. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries, Required Output Artifacts (What files to write/update) (+2 more) Nodes (10): 1. Pre-Deployment Checklist, 2. Build Verification, 3. Deployment Strategy (Based on Target), 4. Post-Deployment Health Check, 5. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries, Required Output Artifacts (What files to write/update) (+2 more)
### Community 113 - "PetsController" ### Community 113 - "PetsController"
Cohesion: 0.11 Cohesion: 0.10
Nodes (13): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+5 more) Nodes (14): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+6 more)
### Community 114 - "AppService" ### Community 114 - "AppService"
Cohesion: 0.29 Cohesion: 0.29
Nodes (5): AppController, Controller, Get, AppService, Injectable Nodes (5): AppController, Controller, Get, AppService, Injectable
### Community 115 - "Spinner.tsx" ### Community 115 - "Spinner.tsx"
Cohesion: 0.07 Cohesion: 0.10
Nodes (36): ImagePreviewModal(), ImagePreviewModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, maxWidthClasses, Modal() (+28 more) Nodes (22): ImagePreviewModal(), ImagePreviewModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, Spinner(), Doctor (+14 more)
### Community 116 - "Vazirmatn Changelog" ### Community 116 - "Vazirmatn Changelog"
Cohesion: 0.18 Cohesion: 0.18
@ -826,13 +821,13 @@ Nodes (9): compilerOptions, esModuleInterop, module, moduleResolution, skipLibCh
Cohesion: 0.20 Cohesion: 0.20
Nodes (9): Compile and run the project, Deployment, Description, License, Project setup, Resources, Run tests, Stay in touch (+1 more) Nodes (9): Compile and run the project, Deployment, Description, License, Project setup, Resources, Run tests, Stay in touch (+1 more)
### Community 122 - "AdminController" ### Community 122 - "AdminService"
Cohesion: 0.17 Cohesion: 0.09
Nodes (12): AdminController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Param (+4 more) Nodes (12): AdminController, ApiBearerAuth, ApiOperation, ApiTags, Controller, Delete, Get, Param (+4 more)
### Community 123 - "UsersService" ### Community 123 - "UsersService"
Cohesion: 0.07 Cohesion: 0.05
Nodes (32): AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+24 more) Nodes (42): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+34 more)
### Community 124 - "Repository Map" ### Community 124 - "Repository Map"
Cohesion: 0.20 Cohesion: 0.20
@ -850,6 +845,10 @@ Nodes (9): name, private, scripts, build, dev, lint, preview, type (+1 more)
Cohesion: 0.20 Cohesion: 0.20
Nodes (9): Arch Linux, Contributors, Install, Known problems for variable version, License, Sahel-Font, To Do (variable), طریقه استفاده از نسخه متغیر variable (+1 more) Nodes (9): Arch Linux, Contributors, Install, Known problems for variable version, License, Sahel-Font, To Do (variable), طریقه استفاده از نسخه متغیر variable (+1 more)
### Community 128 - "Body"
Cohesion: 0.17
Nodes (3): Body, Post, CouponInput
### Community 129 - "Reports.tsx" ### Community 129 - "Reports.tsx"
Cohesion: 0.20 Cohesion: 0.20
Nodes (7): BestSellerItem, CategoryDistItem, COLORS, CustomTooltipProps, ReportData, TopCouponItem, Reports Nodes (7): BestSellerItem, CategoryDistItem, COLORS, CustomTooltipProps, ReportData, TopCouponItem, Reports
@ -891,8 +890,8 @@ Cohesion: 0.22
Nodes (8): Arch Linux, bower, Install, npm, Shabnam Font, yarn, طریقه استفاده در صفحات وب:, نمونه متن Sample: Nodes (8): Arch Linux, bower, Install, npm, Shabnam Font, yarn, طریقه استفاده در صفحات وب:, نمونه متن Sample:
### Community 139 - "layout.tsx" ### Community 139 - "layout.tsx"
Cohesion: 0.43 Cohesion: 0.23
Nodes (5): generateMetadata(), RootLayout(), lalezar, vazirmatn, generateOrganizationSchema() Nodes (8): generateMetadata(), RootLayout(), AnalyticsDeferred(), AnalyticsDeferredProps, Window, lalezar, vazirmatn, generateOrganizationSchema()
### Community 140 - "ErrorBoundary" ### Community 140 - "ErrorBoundary"
Cohesion: 0.22 Cohesion: 0.22
@ -910,9 +909,13 @@ Nodes (7): backend, distMainPath, fs, http, path, { spawn, execSync }, waitForBa
Cohesion: 0.25 Cohesion: 0.25
Nodes (6): app_module_1, core_1, fs, path, swagger_1, yaml Nodes (6): app_module_1, core_1, fs, path, swagger_1, yaml
### Community 144 - "SafeImage.tsx" ### Community 144 - "PodcastPlayerModal.tsx"
Cohesion: 0.10 Cohesion: 0.40
Nodes (20): BackButton(), BackButtonProps, BlogPostClientProps, PLAYBACK_RATES, PodcastInlinePlayer(), PodcastInlinePlayerProps, PLAYBACK_RATES, PodcastPlayerModal() (+12 more) Nodes (3): PLAYBACK_RATES, PodcastPlayerModal(), PodcastPlayerModalProps
### Community 145 - "AdminLoginDto"
Cohesion: 0.29
Nodes (6): AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString, MinLength
### Community 146 - "System Discovery" ### Community 146 - "System Discovery"
Cohesion: 0.25 Cohesion: 0.25
@ -922,17 +925,21 @@ Nodes (7): Active Core Applications, Current Architecture, Evidence-Based Status
Cohesion: 0.16 Cohesion: 0.16
Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, HomeModule, Module (+2 more) Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, HomeModule, Module (+2 more)
### Community 148 - "VerifyOtpDto"
Cohesion: 0.33
Nodes (6): ApiProperty, IsNotEmpty, IsString, Matches, VerifyOtpDto, Length
### Community 149 - "wiki/[slug]/page.tsx"
Cohesion: 0.60
Nodes (5): generateMetadata(), getRelatedProducts(), getWikiTerm(), WikiTermPage(), generateMedicalWebPageSchema()
### Community 150 - "Product Requirement Document (PRD)" ### Community 150 - "Product Requirement Document (PRD)"
Cohesion: 0.29 Cohesion: 0.29
Nodes (6): 1. Executive Vision, 2. Target Audience, 3. Functional Requirements, 4. Non-Functional Requirements (Performance, Security), 5. Epic / Feature Breakdown, Product Requirement Document (PRD) Nodes (6): 1. Executive Vision, 2. Target Audience, 3. Functional Requirements, 4. Non-Functional Requirements (Performance, Security), 5. Epic / Feature Breakdown, Product Requirement Document (PRD)
### Community 151 - "menu.module.ts" ### Community 152 - "RevalidationService"
Cohesion: 0.40 Cohesion: 0.10
Nodes (3): MenuModule, Module, MenuType Nodes (11): Optional, RevalidationModule, Global, Module, RevalidationService, Injectable, RedisModule, Global (+3 more)
### Community 152 - "auth.service.ts"
Cohesion: 0.09
Nodes (11): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, normalizeMobile(), RedisModule, Global (+3 more)
### Community 153 - "exclude" ### Community 153 - "exclude"
Cohesion: 0.22 Cohesion: 0.22
@ -947,8 +954,8 @@ Cohesion: 0.67
Nodes (3): prisma, runSeoBackfill(), stripHtml() Nodes (3): prisma, runSeoBackfill(), stripHtml()
### Community 158 - "media/[...path]/route.ts" ### Community 158 - "media/[...path]/route.ts"
Cohesion: 0.60 Cohesion: 0.53
Nodes (4): dynamic, GET(), getCandidateUrls(), HEAD() Nodes (5): dynamic, GET(), getCandidateUrls(), getMimeType(), HEAD()
### Community 159 - "with-vpn.sh" ### Community 159 - "with-vpn.sh"
Cohesion: 0.62 Cohesion: 0.62
@ -1019,8 +1026,8 @@ Cohesion: 0.40
Nodes (4): activeFiles, errors, validationOutput, warnings Nodes (4): activeFiles, errors, validationOutput, warnings
### Community 176 - "uploads/[...path]/route.ts" ### Community 176 - "uploads/[...path]/route.ts"
Cohesion: 0.60 Cohesion: 0.53
Nodes (4): dynamic, GET(), getCandidateUrls(), HEAD() Nodes (5): dynamic, GET(), getCandidateUrls(), getMimeType(), HEAD()
### Community 177 - "app/page.tsx" ### Community 177 - "app/page.tsx"
Cohesion: 0.67 Cohesion: 0.67
@ -1050,13 +1057,9 @@ Nodes (3): Architectural Overview, Critical Review Findings & Required Enhanceme
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): Overview & Content Foundation, SEO & Content Requirements, 🚀 SEO & Content Strategy Review (12_seo_content) Nodes (3): Overview & Content Foundation, SEO & Content Requirements, 🚀 SEO & Content Strategy Review (12_seo_content)
### Community 184 - "MetricsController" ### Community 184 - "AppModule"
Cohesion: 0.25 Cohesion: 0.12
Nodes (5): ApiExcludeController, MetricsController, Controller, Get, Res Nodes (7): ApiExcludeController, AppModule, Module, MetricsController, Controller, Get, Res
### Community 185 - "RouteErrorBoundary"
Cohesion: 0.22
Nodes (3): Props, RouteErrorBoundary, State
### Community 191 - "graphify reference: add a URL and watch a folder" ### Community 191 - "graphify reference: add a URL and watch a folder"
Cohesion: 0.50 Cohesion: 0.50
@ -1083,8 +1086,8 @@ Cohesion: 0.50
Nodes (3): Select, SelectOption, SelectProps Nodes (3): Select, SelectOption, SelectProps
### Community 197 - "userStore.ts" ### Community 197 - "userStore.ts"
Cohesion: 0.08 Cohesion: 0.06
Nodes (37): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText() (+29 more) Nodes (33): AuthModal, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText(), BrandLogo(), BrandLogoProps (+25 more)
### Community 199 - "SmsLogQueryDto" ### Community 199 - "SmsLogQueryDto"
Cohesion: 0.25 Cohesion: 0.25
@ -1098,10 +1101,6 @@ Nodes (3): Deploy on Vercel, Getting Started, Learn More
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script
### Community 204 - "zibal-ebank.service.ts"
Cohesion: 0.40
Nodes (4): EBankCheckoutListFilter, EBankCheckoutOptions, EBankIdentifiedPaymentFilter, EBankStatementFilter
### Community 211 - "Master Task Backlog (Phase 3.3)" ### Community 211 - "Master Task Backlog (Phase 3.3)"
Cohesion: 0.67 Cohesion: 0.67
Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Management, Master Task Backlog (Phase 3.3), Phase 3 Master Execution Plan Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Management, Master Task Backlog (Phase 3.3), Phase 3 Master Execution Plan
@ -1110,12 +1109,8 @@ Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Managem
Cohesion: 0.67 Cohesion: 0.67
Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font
### Community 233 - "Reviews.tsx"
Cohesion: 0.40
Nodes (5): BlogCommentItem, ProductReview, Reviews(), toPersianDigits(), Reviews
### Community 298 - ".initiateOrderPayment" ### Community 298 - ".initiateOrderPayment"
Cohesion: 0.21 Cohesion: 0.28
Nodes (6): ApiBadRequestResponse, ApiOkResponse, Req, Res, Headers, Ip Nodes (6): ApiBadRequestResponse, ApiOkResponse, Req, Res, Headers, Ip
### Community 317 - "revalidate/route.ts" ### Community 317 - "revalidate/route.ts"
@ -1123,24 +1118,24 @@ Cohesion: 0.83
Nodes (3): GET(), handleRevalidate(), POST() Nodes (3): GET(), handleRevalidate(), POST()
## Knowledge Gaps ## Knowledge Gaps
- **1347 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1342 more) - **1352 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1347 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **120 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **95 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `Roles()` connect `Roles` to `CmsController`, `TicketsService`, `SettingsController`, `CreateReviewDto`, `JwtAuthGuard`, `MenuService`, `WholesaleApplyDto`, `B2BService`, `ContactService`, `FaqService`, `SslController`, `BannersService`, `TestimonialsService`, `IngredientsService`, `PrescriptionsController`, `SmartAdvisorService`, `ProductsService`, `payment.controller.ts`, `cms.controller.ts`?** - **Why does `Roles()` connect `Roles` to `WholesaleApplyDto`, `B2BService`, `ContactService`, `FaqService`, `CmsController`, `tickets.controller.ts`, `SmsService`, `SslController`, `BannersService`, `ProductsService`, `ReviewsService`, `TestimonialsService`, `IngredientsService`, `JwtAuthGuard`, `PrescriptionsService`, `SmartAdvisorService`, `MenuService`?**
_High betweenness centrality (0.065) - this node is a cross-community bridge._ _High betweenness centrality (0.094) - this node is a cross-community bridge._
- **Why does `ApiResponse` connect `src/services/api.ts` to `BlogsController`, `PetsController`, `ProductsService`, `WikiController`, `auth.controller.ts`, `HomeController`, `UsersService`, `OrdersService`?** - **Why does `ApiResponse` connect `src/services/api.ts` to `BlogsController`, `PetsController`, `ProductsService`, `PaginationDto`, `AuthController`, `HomeController`, `UsersService`, `OrdersService`?**
_High betweenness centrality (0.057) - this node is a cross-community bridge._ _High betweenness centrality (0.066) - this node is a cross-community bridge._
- **Why does `PrismaService` connect `PrismaService` to `app.module.ts`, `CmsController`, `TicketsService`, `CreateReviewDto`, `DoctorQueryDto`, `HomeController`, `CreateVideoDto`, `BlogsService`, `menu.module.ts`, `auth.service.ts`, `MenuService`, `B2BService`, `FaqService`, `ZibalService`, `CategoriesController`, `MediaController`, `ZibalEBankService`, `BannersService`, `TestimonialsService`, `IngredientsService`, `SmartAdvisorService`, `MetricsController`, `WikiService`, `admin.service.ts`, `ReportsController`, `PetsController`, `zibal-ebank.service.ts`, `ProductsService`, `seo.module.ts`, `zibal.service.ts`, `OrdersService`, `BlogsController`, `cms.controller.ts`, `PaginationDto`, `PetsController`, `UsersService`?** - **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `admin.module.ts`, `CmsController`, `tickets.controller.ts`, `PaginationDto`, `PetsController`, `ProductsService`, `DoctorQueryDto`, `PetsController`, `admin.controller.ts`, `CreateVideoDto`, `auth.service.ts`, `UsersService`, `OrdersService`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._ _High betweenness centrality (0.030) - this node is a cross-community bridge._
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?** - **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
_1347 weakly-connected nodes found - possible documentation gaps or missing edges._ _1352 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `app.module.ts` be split into smaller, more focused modules?** - **Should `app.module.ts` be split into smaller, more focused modules?**
_Cohesion score 0.06170598911070781 - nodes in this community are weakly interconnected._ _Cohesion score 0.06988120195667366 - nodes in this community are weakly interconnected._
- **Should `productService.ts` be split into smaller, more focused modules?** - **Should `productService.ts` be split into smaller, more focused modules?**
_Cohesion score 0.05780885780885781 - nodes in this community are weakly interconnected._ _Cohesion score 0.05472837022132797 - nodes in this community are weakly interconnected._
- **Should `PetProfile.tsx` be split into smaller, more focused modules?** - **Should `UserDashboard.tsx` be split into smaller, more focused modules?**
_Cohesion score 0.11822660098522167 - nodes in this community are weakly interconnected._ _Cohesion score 0.10752688172043011 - nodes in this community are weakly interconnected._

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff