Compare commits

..

No commits in common. "5ace474d71f8132ffe015e56fda81fed9849076e" and "8992060cb74cb3647ecdbf06d6437ecd23d9313f" have entirely different histories.

27 changed files with 33841 additions and 62989 deletions

View File

@ -583,23 +583,3 @@ model Setting {
@@index([category]) @@index([category])
@@map("settings") @@map("settings")
} }
model SmsLog {
id String @id @default(uuid()) @db.Uuid
receptor String @db.VarChar(20)
type String @db.VarChar(50) // OTP, ORDER_CONFIRMATION, SHIPPING_TRACKING, B2B_NOTIFICATION, PET_CARE_REMINDER, TEST, GENERIC
patternId Int? @map("pattern_id")
args String[] @default([])
messageText String? @map("message_text") @db.Text
status String @db.VarChar(20) // SUCCESS, FAILED, DISABLED
recId String? @map("rec_id") @db.VarChar(50)
errorMessage String? @map("error_message") @db.Text
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
@@index([receptor])
@@index([type])
@@index([status])
@@index([createdAt])
@@map("sms_logs")
}

View File

@ -29,8 +29,6 @@ import { PrescriptionsModule } from './prescriptions/prescriptions.module';
import { B2BModule } from './b2b/b2b.module'; import { B2BModule } from './b2b/b2b.module';
import { PaymentModule } from './payment/payment.module'; import { PaymentModule } from './payment/payment.module';
import { Request, Response, NextFunction } from 'express';
@Module({ @Module({
imports: [ imports: [
SmsModule, SmsModule,
@ -79,18 +77,15 @@ export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer consumer
.apply((req: Request, res: Response, next: NextFunction) => { .apply((req: any, res: any, next: () => void) => {
MetricsController.incrementRequestCount(); MetricsController.incrementRequestCount();
const url: string = req.originalUrl || req.url || ''; const url: string = req.originalUrl || req.url || '';
const method: string = req.method || ''; const method: string = req.method || '';
// Exclude options, admin panel requests, static assets, and health metrics from visit counter // Exclude options, admin panel requests, static assets, and health metrics from visit counter
const isAdminPath = const isAdminPath = url.includes('/admin') || url.includes('/api/admin');
url.includes('/admin') || url.includes('/api/admin'); const isStaticAsset = url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i);
const isStaticAsset = url.match(
/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i,
);
const isIgnoredMethod = method === 'OPTIONS' || method === 'HEAD'; const isIgnoredMethod = method === 'OPTIONS' || method === 'HEAD';
if (!isAdminPath && !isStaticAsset && !isIgnoredMethod) { if (!isAdminPath && !isStaticAsset && !isIgnoredMethod) {

View File

@ -49,8 +49,7 @@ export class AuthService {
// Remove OTP from Redis if SMS failed to avoid phantom codes // Remove OTP from Redis if SMS failed to avoid phantom codes
await this.redisService.del(`otp:${phoneNumber}`); await this.redisService.del(`otp:${phoneNumber}`);
throw new BadRequestException({ throw new BadRequestException({
message: message: 'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
error: 'SMS_DELIVERY_FAILED', error: 'SMS_DELIVERY_FAILED',
}); });
} }

View File

@ -1,646 +1,40 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import * as https from 'https'; import * as https from 'https';
import { PrismaService } from '../../prisma/prisma.service';
import { Prisma } from '@prisma/client';
export interface SendPatternSmsOptions { export interface SendPatternSmsOptions {
to: string; to: string;
bodyId: number; // MeliPayamak Shared Pattern Body ID bodyId: number; // MeliPayamak Shared Pattern Body ID
args: string[]; // Dynamic variables inside pattern args: string[]; // Dynamic variables inside pattern
type?: string; // OTP, ORDER_CONFIRMATION, SHIPPING_TRACKING, B2B_NOTIFICATION, PET_CARE_REMINDER, TEST, GENERIC
} }
export interface SmsConfig { interface MeliPayamakResponse {
enabled: boolean;
username: string;
password: string;
fromNumber?: string;
otpBodyId: number;
orderBodyId: number;
shippingBodyId: number;
b2bBodyId: number;
petCareBodyId: number;
}
export interface MeliPayamakPattern {
id: number;
title: string;
body: string;
status: number; // 0 = Pending, 1 = Approved, 2 = Needs Edit
statusText?: string;
assignedTo?: string[];
}
export class SmsLogQuery {
page?: number;
limit?: number;
search?: string;
type?: string;
status?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface MeliPayamakResponse {
Value?: number; Value?: number;
RetStatus?: number; RetStatus?: number;
StrRetStatus?: string;
} }
@Injectable() @Injectable()
export class SmsService { export class SmsService {
private readonly logger = new Logger(SmsService.name); private readonly logger = new Logger(SmsService.name);
private readonly username = process.env.MELIPAYAMAK_USERNAME || '9364100228';
constructor(private readonly prisma: PrismaService) {} private readonly password = process.env.MELIPAYAMAK_PASSWORD || '';
/**
* Get active SMS configuration from database (fallback to .env)
*/
async getSmsConfig(): Promise<SmsConfig> {
try {
const setting = await this.prisma.setting.findFirst({
where: { category: 'sms' },
});
const dbConfig = (setting?.value as Record<string, unknown>) || {};
return {
enabled:
dbConfig.enabled !== undefined ? Boolean(dbConfig.enabled) : true,
username:
typeof dbConfig.username === 'string' && dbConfig.username
? dbConfig.username
: process.env.MELIPAYAMAK_USERNAME || '9364100228',
password:
typeof dbConfig.password === 'string' && dbConfig.password
? dbConfig.password
: process.env.MELIPAYAMAK_PASSWORD || '',
fromNumber:
typeof dbConfig.fromNumber === 'string' && dbConfig.fromNumber
? dbConfig.fromNumber
: process.env.MELIPAYAMAK_FROM_NUMBER || '',
otpBodyId: Number(
dbConfig.otpBodyId || process.env.MELIPAYAMAK_OTP_BODY_ID || '508079',
),
orderBodyId: Number(
dbConfig.orderBodyId ||
process.env.MELIPAYAMAK_ORDER_BODY_ID ||
'508081',
),
shippingBodyId: Number(
dbConfig.shippingBodyId ||
process.env.MELIPAYAMAK_SHIPPING_BODY_ID ||
'508082',
),
b2bBodyId: Number(
dbConfig.b2bBodyId || process.env.MELIPAYAMAK_B2B_BODY_ID || '508083',
),
petCareBodyId: Number(
dbConfig.petCareBodyId ||
process.env.MELIPAYAMAK_PET_CARE_BODY_ID ||
'0',
),
};
} catch {
return {
enabled: true,
username: process.env.MELIPAYAMAK_USERNAME || '9364100228',
password: process.env.MELIPAYAMAK_PASSWORD || '',
fromNumber: process.env.MELIPAYAMAK_FROM_NUMBER || '',
otpBodyId: Number(process.env.MELIPAYAMAK_OTP_BODY_ID || '508079'),
orderBodyId: Number(process.env.MELIPAYAMAK_ORDER_BODY_ID || '508081'),
shippingBodyId: Number(
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '508082',
),
b2bBodyId: Number(process.env.MELIPAYAMAK_B2B_BODY_ID || '508083'),
petCareBodyId: Number(process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0'),
};
}
}
/**
* Helper HTTP POST request to Payamak-Panel ASMX endpoints
*/
private async postAsmx(
endpoint: string,
params: Record<string, string | number>,
): Promise<string> {
const postData = Object.entries(params)
.map(
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`,
)
.join('&');
return new Promise((resolve, reject) => {
const req = https.request(
`https://api.payamak-panel.com/post/SharedService.asmx/${endpoint}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData),
},
},
(res) => {
let data = '';
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
res.on('end', () => resolve(data));
},
);
req.on('error', (err: Error) => reject(err));
req.write(postData);
req.end();
});
}
/**
* Helper to write an SMS Log to PostgreSQL safely
*/
private async recordLog(entry: {
receptor: string;
type: string;
patternId?: number;
args?: string[];
messageText?: string;
status: string;
recId?: string;
errorMessage?: string;
}): Promise<void> {
try {
await this.prisma.smsLog.create({
data: {
receptor: entry.receptor,
type: entry.type || 'GENERIC',
patternId: entry.patternId || null,
args: entry.args || [],
messageText: entry.messageText || null,
status: entry.status,
recId: entry.recId ? String(entry.recId) : null,
errorMessage: entry.errorMessage || null,
},
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(`[SMS Log Save Failed]: ${msg}`);
}
}
/**
* Query paginated SMS Logs with Search, Filter and Sorting
*/
async getSmsLogs(query: SmsLogQuery = {}) {
const page = Math.max(1, Number(query.page) || 1);
const limit = Math.max(1, Math.min(100, Number(query.limit) || 15));
const skip = (page - 1) * limit;
const where: Prisma.SmsLogWhereInput = {};
if (query.search) {
const s = query.search.trim();
where.OR = [
{ receptor: { contains: s, mode: 'insensitive' } },
{ recId: { contains: s, mode: 'insensitive' } },
{ errorMessage: { contains: s, mode: 'insensitive' } },
];
}
if (query.type && query.type !== 'ALL') {
where.type = query.type;
}
if (query.status && query.status !== 'ALL') {
where.status = query.status;
}
const sortField = query.sortBy || 'createdAt';
const sortOrder = query.sortOrder === 'asc' ? 'asc' : 'desc';
const [logs, total, totalSuccess, totalFailed] = await Promise.all([
this.prisma.smsLog.findMany({
where,
skip,
take: limit,
orderBy: { [sortField]: sortOrder },
}),
this.prisma.smsLog.count({ where }),
this.prisma.smsLog.count({ where: { status: 'SUCCESS' } }),
this.prisma.smsLog.count({ where: { status: 'FAILED' } }),
]);
return {
logs,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
stats: {
total,
success: totalSuccess,
failed: totalFailed,
successRate: total > 0 ? Math.round((totalSuccess / total) * 100) : 100,
},
};
}
/**
* Delete specific SMS log entry
*/
async deleteSmsLog(id: string) {
return this.prisma.smsLog.delete({ where: { id } });
}
/**
* Clear all SMS logs
*/
async clearAllSmsLogs() {
return this.prisma.smsLog.deleteMany();
}
/**
* Parse XML response for GetSharedServiceBody into structured pattern array
*/
private parsePatternsXml(xml: string): MeliPayamakPattern[] {
const patterns: MeliPayamakPattern[] = [];
const itemRegex = /<SharedServiceBody>([\s\S]*?)<\/SharedServiceBody>/gi;
let match: RegExpExecArray | null;
while ((match = itemRegex.exec(xml)) !== null) {
const block = match[1];
const idMatch = block.match(/<Id>(\d+)<\/Id>/i);
const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i);
const statusMatch = block.match(/<Status>(-?\d+)<\/Status>/i);
if (idMatch) {
const id = parseInt(idMatch[1], 10);
const title = (titleMatch ? titleMatch[1] : '')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
const body = (bodyMatch ? bodyMatch[1] : '')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
let statusText = 'در انتظار تایید';
if (status === 1) statusText = 'تایید شده';
else if (status === 2 || status === -1) statusText = 'نیاز به ویرایش';
patterns.push({ id, title, body, status, statusText });
}
}
return patterns;
}
/**
* Fetch all registered patterns from MeliPayamak (with local cache sync)
*/
async getPatterns(): Promise<MeliPayamakPattern[]> {
const config = await this.getSmsConfig();
let remotePatterns: MeliPayamakPattern[] = [];
if (config.username && config.password) {
try {
const rawXml = await this.postAsmx('GetSharedServiceBody', {
username: config.username,
password: config.password,
});
remotePatterns = this.parsePatternsXml(rawXml);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.warn(
`[SMS Patterns] Remote fetch failed: ${msg}. Using stored patterns.`,
);
}
}
// Load local stored patterns from settings table
const storedSetting = await this.prisma.setting.findFirst({
where: { category: 'sms_patterns' },
});
const localPatterns: MeliPayamakPattern[] = Array.isArray(
storedSetting?.value,
)
? (storedSetting.value as unknown as MeliPayamakPattern[])
: [];
// Merge remote and local (remote takes precedence on ID match)
const map = new Map<number, MeliPayamakPattern>();
// Add configured system patterns by default if map is empty
const defaultConfigs: Array<{
id: number;
title: string;
body: string;
assigned: string;
}> = [
{
id: config.otpBodyId,
title: 'کد تایید ورود و ثبت‌نام (OTP)',
body: 'کد ورود به کانینا: {0}',
assigned: 'otp',
},
{
id: config.orderBodyId,
title: 'تایید و ثبت فاکتور سفارش',
body: 'سفارش شما به شماره {0} با مبلغ {1} ثبت گردید.',
assigned: 'order',
},
{
id: config.shippingBodyId,
title: 'ارسال کد رهگیری پستی',
body: 'مرسوله سفارش {0} با کد رهگیری پستی {1} ارسال شد.',
assigned: 'shipping',
},
{
id: config.b2bBodyId,
title: 'اطلاع‌رسانی درخواست B2B',
body: 'همکار گرامی {0} درخواست شما دریافت شد.',
assigned: 'b2b',
},
];
for (const d of defaultConfigs) {
if (d.id > 0) {
map.set(d.id, {
id: d.id,
title: d.title,
body: d.body,
status: 1,
statusText: 'تایید شده',
assignedTo: [d.assigned],
});
}
}
for (const lp of localPatterns) {
if (lp && lp.id) map.set(lp.id, { ...lp });
}
for (const rp of remotePatterns) {
const existing = map.get(rp.id);
map.set(rp.id, {
...rp,
assignedTo: existing?.assignedTo || [],
});
}
// Mark current bindings
const result = Array.from(map.values()).map((p) => {
const assigned: string[] = [];
if (p.id === config.otpBodyId) assigned.push('کد تایید OTP');
if (p.id === config.orderBodyId) assigned.push('تایید سفارش');
if (p.id === config.shippingBodyId) assigned.push('کد رهگیری پست');
if (p.id === config.b2bBodyId) assigned.push('همکاران B2B');
if (p.id === config.petCareBodyId) assigned.push('یادآور سلامت پت');
return { ...p, assignedTo: assigned };
});
return result;
}
/**
* Add a new pattern to MeliPayamak
*/
async addPattern(
title: string,
body: string,
blackListId: number = 0,
): Promise<{ success: boolean; bodyId?: number; message: string }> {
const config = await this.getSmsConfig();
if (!config.username || !config.password) {
return {
success: false,
message: 'مشخصات نام کاربری و رمز عبور سامانه پیامک تنظیم نشده است.',
};
}
try {
const rawXml = await this.postAsmx('SharedServiceBodyAdd', {
username: config.username,
password: config.password,
title,
body,
blackListId,
});
const match =
rawXml.match(/<int.*?>(-?\d+)<\/int>/i) || rawXml.match(/>(-?\d+)</);
const code = match ? parseInt(match[1], 10) : 0;
if (code > 0) {
await this.saveLocalPattern({
id: code,
title,
body,
status: 0,
statusText: 'در انتظار تایید',
});
return {
success: true,
bodyId: code,
message: `الگو با موفقیت ثبت شد و شناسه اختصاصی ${code} دریافت گردید. (وضعیت: در انتظار تایید ناظر)`,
};
} else if (code === 0) {
return {
success: false,
message: 'نام کاربری یا کلمه عبور ملی پیامک اشتباه است.',
};
} else if (code === -2) {
return {
success: false,
message: 'شناسه لیست سیاه ویژه اشتباه است.',
};
} else {
return {
success: false,
message: `خطا در ثبت پترن با کد پاسخ: ${code}`,
};
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
const generatedId = Math.floor(500000 + Math.random() * 90000);
await this.saveLocalPattern({
id: generatedId,
title,
body,
status: 0,
statusText: 'ثبت محلی (در انتظار اتصال سرور)',
});
return {
success: true,
bodyId: generatedId,
message: `الگو به صورت محلی با شناسه موقت ${generatedId} ذخیره شد (${msg}).`,
};
}
}
/**
* Edit an existing pattern in MeliPayamak
*/
async editPattern(
bodyId: number,
body: string,
): Promise<{ success: boolean; message: string }> {
const config = await this.getSmsConfig();
if (!config.username || !config.password) {
return {
success: false,
message: 'مشخصات نام کاربری و رمز عبور سامانه پیامک تنظیم نشده است.',
};
}
try {
const rawXml = await this.postAsmx('SharedServiceBodyEdit', {
username: config.username,
password: config.password,
bodyId,
body,
});
const match =
rawXml.match(/<int.*?>(-?\d+)<\/int>/i) || rawXml.match(/>(-?\d+)</);
const code = match ? parseInt(match[1], 10) : 0;
if (code === 1) {
await this.updateLocalPattern(bodyId, body);
return {
success: true,
message:
'ویرایش الگو با موفقیت انجام شد و منتظر تایید مجدد ناظر می‌باشد.',
};
} else if (code === -1) {
await this.updateLocalPattern(bodyId, body);
return {
success: true,
message:
'متن الگو در سیستم بروز شد (توجه: فقط الگوهای در وضعیت نیاز به ویرایش در وب‌سرویس اصلاح می‌شوند).',
};
} else {
return {
success: false,
message: `خطا در ویرایش الگو با کد پاسخ: ${code}`,
};
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
await this.updateLocalPattern(bodyId, body);
return {
success: true,
message: `متن الگو در دیتابیس داخلی بروز شد (${msg}).`,
};
}
}
private async saveLocalPattern(pattern: MeliPayamakPattern) {
try {
const setting = await this.prisma.setting.findFirst({
where: { category: 'sms_patterns' },
});
const list: MeliPayamakPattern[] = Array.isArray(setting?.value)
? (setting.value as unknown as MeliPayamakPattern[])
: [];
const updated = [pattern, ...list.filter((p) => p.id !== pattern.id)];
await this.prisma.setting.upsert({
where: { key: 'sms_patterns_config' },
update: {
category: 'sms_patterns',
value: updated as unknown as Prisma.InputJsonValue,
},
create: {
key: 'sms_patterns_config',
category: 'sms_patterns',
value: updated as unknown as Prisma.InputJsonValue,
},
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(`Failed to save local pattern: ${msg}`);
}
}
private async updateLocalPattern(bodyId: number, newBody: string) {
try {
const setting = await this.prisma.setting.findFirst({
where: { category: 'sms_patterns' },
});
const list: MeliPayamakPattern[] = Array.isArray(setting?.value)
? (setting.value as unknown as MeliPayamakPattern[])
: [];
const updated = list.map((p) =>
p.id === bodyId
? {
...p,
body: newBody,
status: 0,
statusText: 'در انتظار تایید',
}
: p,
);
await this.prisma.setting.upsert({
where: { key: 'sms_patterns_config' },
update: {
category: 'sms_patterns',
value: updated as unknown as Prisma.InputJsonValue,
},
create: {
key: 'sms_patterns_config',
category: 'sms_patterns',
value: updated as unknown as Prisma.InputJsonValue,
},
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(`Failed to update local pattern: ${msg}`);
}
}
/** /**
* Send Pattern SMS using MeliPayamak Shared Service Line (Bypasses Blacklist) * Send Pattern SMS using MeliPayamak Shared Service Line (Bypasses Blacklist)
*/ */
async sendPatternSms(options: SendPatternSmsOptions): Promise<boolean> { async sendPatternSms(options: SendPatternSmsOptions): Promise<boolean> {
const config = await this.getSmsConfig(); if (!this.username || !this.password) {
const msgType = options.type || 'GENERIC_PATTERN'; this.logger.error(
`[SMS MISCONFIGURED] MELIPAYAMAK_PASSWORD is not set! ` +
if (!config.enabled) { `SMS to ${options.to} (Pattern: ${options.bodyId}) was NOT sent. ` +
this.logger.warn(`[SMS Disabled] SMS dispatch skipped for ${options.to}`); `Set MELIPAYAMAK_USERNAME and MELIPAYAMAK_PASSWORD environment variables.`,
await this.recordLog({ );
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'DISABLED',
errorMessage: 'ارسال پیامک از پنل ادمین غیرفعال شده است.',
});
return false;
}
if (!config.username || !config.password) {
const err = 'مشخصات نام کاربری و رمز عبور سامانه پیامک تنظیم نشده است.';
this.logger.error(`[SMS MISCONFIGURED] ${err}`);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'FAILED',
errorMessage: err,
});
return false; return false;
} }
return new Promise((resolve) => { return new Promise((resolve) => {
const payload = JSON.stringify({ const payload = JSON.stringify({
username: config.username, username: this.username,
password: config.password, password: this.password,
text: options.args.join(';'), text: options.args.join(';'),
to: options.to, to: options.to,
bodyId: options.bodyId, bodyId: options.bodyId,
@ -657,9 +51,8 @@ export class SmsService {
}, },
(res) => { (res) => {
let data = ''; let data = '';
res.on('data', (chunk: Buffer | string) => (data += String(chunk))); res.on('data', (chunk) => (data += chunk));
res.on('end', () => { res.on('end', () => {
void (async () => {
try { try {
const json = JSON.parse(data) as MeliPayamakResponse; const json = JSON.parse(data) as MeliPayamakResponse;
const val = json.Value ?? 0; const val = json.Value ?? 0;
@ -667,63 +60,25 @@ export class SmsService {
this.logger.log( this.logger.log(
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`, `[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
); );
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'SUCCESS',
recId: String(val),
});
resolve(true); resolve(true);
} else { } else {
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
this.logger.error( this.logger.error(
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`, `[SMS Error] Failed sending pattern SMS to ${options.to}. Code: ${val}`,
); );
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'FAILED',
recId: String(val),
errorMessage: errMsg,
});
resolve(false); resolve(false);
} }
} catch (err: unknown) { } catch {
const msg = err instanceof Error ? err.message : String(err);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'FAILED',
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
});
resolve(false); resolve(false);
} }
})();
}); });
}, },
); );
req.on('error', (err: Error) => { req.on('error', (err) => {
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({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
status: 'FAILED',
errorMessage: `خطای شبکه: ${err.message}`,
});
resolve(false); resolve(false);
})();
}); });
req.write(payload); req.write(payload);
@ -732,190 +87,73 @@ export class SmsService {
} }
/** /**
* Test SMS Dispatch from Admin Panel * Send OTP Verification Code (Pattern 508079)
*/
async testSms(
targetPhone: string,
testPatternId?: number,
testArgs?: string[],
): Promise<{
success: boolean;
message: string;
rawResponse?: MeliPayamakResponse;
}> {
const config = await this.getSmsConfig();
if (!config.username || !config.password) {
const msg = 'نام کاربری یا رمز عبور سامانه ملی پیامک تنظیم نشده است.';
await this.recordLog({
receptor: targetPhone,
type: 'TEST',
patternId: testPatternId,
args: testArgs,
status: 'FAILED',
errorMessage: msg,
});
return { success: false, message: msg };
}
const bodyId = testPatternId || config.otpBodyId || 508079;
const args = testArgs && testArgs.length > 0 ? testArgs : ['123456'];
return new Promise((resolve) => {
const payload = JSON.stringify({
username: config.username,
password: config.password,
text: args.join(';'),
to: targetPhone,
bodyId: bodyId,
});
const req = https.request(
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
(res) => {
let data = '';
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
res.on('end', () => {
void (async () => {
try {
const json = JSON.parse(data) as MeliPayamakResponse;
const val = json.Value ?? 0;
if (json && (val > 15 || json.RetStatus === 1)) {
await this.recordLog({
receptor: targetPhone,
type: 'TEST',
patternId: bodyId,
args,
status: 'SUCCESS',
recId: String(val),
});
resolve({
success: true,
message: `پیامک تستی پترن با موفقیت ارسال شد (شناسه پیگیری ملی پیامک: ${val})`,
rawResponse: json,
});
} else {
const errMsg = `خطای درگاه ملی پیامک: کد پاسخ بازگشتی ${val}`;
await this.recordLog({
receptor: targetPhone,
type: 'TEST',
patternId: bodyId,
args,
status: 'FAILED',
recId: String(val),
errorMessage: errMsg,
});
resolve({
success: false,
message: errMsg,
rawResponse: json,
});
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
const errMsg = `پاسخ نامعتبر از سرور ملی پیامک: ${data || msg}`;
await this.recordLog({
receptor: targetPhone,
type: 'TEST',
patternId: bodyId,
args,
status: 'FAILED',
errorMessage: errMsg,
});
resolve({ success: false, message: errMsg });
}
})();
});
},
);
req.on('error', (err: Error) => {
void (async () => {
const errMsg = `خطای برقراری ارتباط با وب‌سرویس ملی پیامک: ${err.message}`;
await this.recordLog({
receptor: targetPhone,
type: 'TEST',
patternId: bodyId,
args,
status: 'FAILED',
errorMessage: errMsg,
});
resolve({ success: false, message: errMsg });
})();
});
req.write(payload);
req.end();
});
}
/**
* Send OTP Verification Code
*/ */
async sendOtp(phone: string, otpCode: string): Promise<boolean> { async sendOtp(phone: string, otpCode: string): Promise<boolean> {
const config = await this.getSmsConfig(); const bodyId = parseInt(
process.env.MELIPAYAMAK_OTP_BODY_ID || '508079',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId: config.otpBodyId, bodyId,
args: [otpCode], args: [otpCode],
type: 'OTP',
}); });
} }
/** /**
* Send Order Confirmation SMS * Send Order Confirmation SMS (Pattern 508081)
*/ */
async sendOrderConfirmation( async sendOrderConfirmation(
phone: string, phone: string,
orderNumber: string, orderNumber: string,
amount: string, amount: string,
): Promise<boolean> { ): Promise<boolean> {
const config = await this.getSmsConfig(); const bodyId = parseInt(
process.env.MELIPAYAMAK_ORDER_BODY_ID || '508081',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId: config.orderBodyId, bodyId,
args: [orderNumber, amount], args: [orderNumber, amount],
type: 'ORDER_CONFIRMATION',
}); });
} }
/** /**
* Send Shipping Status SMS with Tracking Code * Send Shipping Status SMS with Tracking Code (Pattern 508082)
*/ */
async sendShippingNotification( async sendShippingNotification(
phone: string, phone: string,
orderNumber: string, orderNumber: string,
trackingCode: string, trackingCode: string,
): Promise<boolean> { ): Promise<boolean> {
const config = await this.getSmsConfig(); const bodyId = parseInt(
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '508082',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId: config.shippingBodyId, bodyId,
args: [orderNumber, trackingCode], args: [orderNumber, trackingCode],
type: 'SHIPPING_TRACKING',
}); });
} }
/** /**
* Send B2B Application Notification SMS * Send B2B Application Notification SMS (Pattern 508083)
*/ */
async sendB2bNotification( async sendB2bNotification(
phone: string, phone: string,
applicantName: string, applicantName: string,
): Promise<boolean> { ): Promise<boolean> {
const config = await this.getSmsConfig(); const bodyId = parseInt(
process.env.MELIPAYAMAK_B2B_BODY_ID || '508083',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId: config.b2bBodyId, bodyId,
args: [applicantName], args: [applicantName],
type: 'B2B_NOTIFICATION',
}); });
} }
@ -927,13 +165,14 @@ export class SmsService {
petName: string, petName: string,
reminderType: string, reminderType: string,
): Promise<boolean> { ): Promise<boolean> {
const config = await this.getSmsConfig(); const bodyId = parseInt(
if (!config.petCareBodyId) return false; process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId: config.petCareBodyId, bodyId,
args: [petName, reminderType], args: [petName, reminderType],
type: 'PET_CARE_REMINDER',
}); });
} }
@ -941,24 +180,7 @@ export class SmsService {
* Generic text SMS dispatch * Generic text SMS dispatch
*/ */
async sendSms(phone: string, message: string): Promise<boolean> { async sendSms(phone: string, message: string): Promise<boolean> {
const config = await this.getSmsConfig();
if (!config.enabled) {
await this.recordLog({
receptor: phone,
type: 'GENERIC_TEXT',
messageText: message,
status: 'DISABLED',
errorMessage: 'ارسال پیامک غیرفعال است.',
});
return false;
}
this.logger.log(`[SMS Text Sent] To: ${phone}, Content: ${message}`); this.logger.log(`[SMS Text Sent] To: ${phone}, Content: ${message}`);
await this.recordLog({
receptor: phone,
type: 'GENERIC_TEXT',
messageText: message,
status: 'SUCCESS',
});
return Promise.resolve(true); return Promise.resolve(true);
} }
} }

View File

@ -1,69 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsNumber, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
export class AdminTransactionFilterDto {
@ApiPropertyOptional({ description: 'شماره صفحه', default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
page?: number = 1;
@ApiPropertyOptional({ description: 'تعداد در هر صفحه', default: 15 })
@IsOptional()
@Type(() => Number)
@IsNumber()
limit?: number = 15;
@ApiPropertyOptional({ description: 'جستجو (نام، موبایل، کد پیگیری، شماره مرجع، کد رهگیری)' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'فیلتر وضعیت (VERIFIED, PENDING, FAILED)' })
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: 'فیلتر درگاه (zibal, wallet, card)' })
@IsOptional()
@IsString()
gateway?: string;
@ApiPropertyOptional({ description: 'فیلتر نوع تراکنش (ORDER, WALLET_TOPUP)' })
@IsOptional()
@IsString()
type?: string;
@ApiPropertyOptional({ description: 'از تاریخ (ISO format)' })
@IsOptional()
@IsString()
startDate?: string;
@ApiPropertyOptional({ description: 'تا تاریخ (ISO format)' })
@IsOptional()
@IsString()
endDate?: string;
@ApiPropertyOptional({ description: 'حداقل مبلغ (تومان)' })
@IsOptional()
@Type(() => Number)
@IsNumber()
minAmount?: number;
@ApiPropertyOptional({ description: 'حداکثر مبلغ (تومان)' })
@IsOptional()
@Type(() => Number)
@IsNumber()
maxAmount?: number;
@ApiPropertyOptional({ description: 'فیلد مرتب‌سازی', default: 'createdAt' })
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@ApiPropertyOptional({ description: 'جهت مرتب‌سازی (asc, desc)', default: 'desc' })
@IsOptional()
@IsIn(['asc', 'desc'])
sortOrder?: 'asc' | 'desc' = 'desc';
}

View File

@ -18,19 +18,17 @@ import {
InitiateWalletTopupDto, InitiateWalletTopupDto,
} from './dto/initiate-payment.dto'; } from './dto/initiate-payment.dto';
import { ZibalCallbackQueryDto } from './dto/verify-payment.dto'; import { ZibalCallbackQueryDto } from './dto/verify-payment.dto';
import { AdminTransactionFilterDto } from './dto/admin-transaction-filter.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
import { import {
ApiTags, ApiTags,
ApiOperation, ApiOperation,
ApiBearerAuth, ApiBearerAuth,
ApiResponse,
ApiOkResponse, ApiOkResponse,
ApiBadRequestResponse, ApiBadRequestResponse,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
@ApiTags('Payment - درگاه پرداخت اینترنتی زیبال و مدیریت تراکنش‌ها') @ApiTags('Payment - درگاه پرداخت اینترنتی زیبال (Zibal IPG)')
@Controller('payment') @Controller('payment')
export class PaymentController { export class PaymentController {
constructor( constructor(
@ -54,9 +52,7 @@ export class PaymentController {
}, },
}, },
}) })
@ApiBadRequestResponse({ @ApiBadRequestResponse({ description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش' })
description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش',
})
async initiateOrderPayment( async initiateOrderPayment(
@Req() req: { user: { id: string } }, @Req() req: { user: { id: string } },
@Body() body: InitiatePaymentDto, @Body() body: InitiatePaymentDto,
@ -74,6 +70,14 @@ export class PaymentController {
@ApiOperation({ summary: 'شارژ آنلاین کیف پول از طریق درگاه زیبال' }) @ApiOperation({ summary: 'شارژ آنلاین کیف پول از طریق درگاه زیبال' })
@ApiOkResponse({ @ApiOkResponse({
description: 'لینک هدایت به درگاه پرداخت برای شارژ کیف پول', description: 'لینک هدایت به درگاه پرداخت برای شارژ کیف پول',
schema: {
example: {
success: true,
trackId: '15966442233311',
paymentUrl: 'https://gateway.zibal.ir/start/15966442233311',
amount: 500000,
},
},
}) })
async initiateWalletTopup( async initiateWalletTopup(
@Req() req: { user: { id: string } }, @Req() req: { user: { id: string } },
@ -87,9 +91,7 @@ export class PaymentController {
} }
@Get('zibal/callback') @Get('zibal/callback')
@ApiOperation({ @ApiOperation({ summary: 'دریافت کال‌بک بازگشت از درگاه زیبال و تایید تراکنش' })
summary: 'دریافت کال‌بک بازگشت از درگاه زیبال و تایید تراکنش',
})
async handleZibalCallback( async handleZibalCallback(
@Query() query: ZibalCallbackQueryDto, @Query() query: ZibalCallbackQueryDto,
@Res() res: Response, @Res() res: Response,
@ -128,10 +130,9 @@ export class PaymentController {
}); });
return res.redirect(`${frontendUrl}/payment/verify?${params.toString()}`); return res.redirect(`${frontendUrl}/payment/verify?${params.toString()}`);
} catch (err: unknown) { } catch (err: any) {
const msg = err instanceof Error ? err.message : 'خطا در پردازش پرداخت';
return res.redirect( return res.redirect(
`${frontendUrl}/payment/verify?success=0&trackId=${trackId}&message=${encodeURIComponent(msg)}`, `${frontendUrl}/payment/verify?success=0&trackId=${trackId}&message=${encodeURIComponent(err.message || 'خطا در پردازش پرداخت')}`,
); );
} }
} }
@ -148,9 +149,7 @@ export class PaymentController {
const orderId = body.orderId; const orderId = body.orderId;
if (!trackId) { if (!trackId) {
return res return res.status(HttpStatus.BAD_REQUEST).json({ success: false, message: 'trackId is required' });
.status(HttpStatus.BAD_REQUEST)
.json({ success: false, message: 'trackId is required' });
} }
const result = await this.paymentService.verifyAndProcess( const result = await this.paymentService.verifyAndProcess(
@ -168,42 +167,4 @@ export class PaymentController {
async getTransactionStatus(@Param('trackId') trackId: string) { async getTransactionStatus(@Param('trackId') trackId: string) {
return this.paymentService.getTransaction(trackId); return this.paymentService.getTransaction(trackId);
} }
// --- ADMIN ENDPOINTS FOR TRANSACTION LOGS & DIAGNOSTICS ---
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('admin/transactions')
@ApiOperation({ summary: 'مدیریت و مشاهده تمامی لاگ‌ها و تراکنش‌های پرداخت' })
async getAdminTransactions(@Query() query: AdminTransactionFilterDto) {
return this.paymentService.getAdminTransactions(query);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('admin/stats')
@ApiOperation({ summary: 'آمار کلی تراکنش‌ها و حجم مبالغ پرداختی' })
async getAdminStats() {
return this.paymentService.getAdminTransactionStats();
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Post('admin/reconcile')
@ApiOperation({ summary: 'اجرای فرایند انطباق و استعلام خودکار تراکنش‌های بلاتکلیف' })
async reconcilePending() {
return this.paymentService.reconcilePendingTransactions();
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('admin/live-inquiry/:id')
@ApiOperation({ summary: 'استعلام زنده وضعیت تراکنش از سرور زیبال' })
async liveInquiry(@Param('id') id: string) {
return this.paymentService.adminLiveInquiry(id);
}
} }

View File

@ -8,7 +8,6 @@ import { PrismaService } from '../prisma/prisma.service';
import { ZibalService } from './zibal.service'; import { ZibalService } from './zibal.service';
import { SmsService } from '../common/services/sms.service'; import { SmsService } from '../common/services/sms.service';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { AdminTransactionFilterDto } from './dto/admin-transaction-filter.dto';
@Injectable() @Injectable()
export class PaymentService { export class PaymentService {
@ -37,20 +36,21 @@ export class PaymentService {
throw new NotFoundException('سفارش مورد نظر یافت نشد'); throw new NotFoundException('سفارش مورد نظر یافت نشد');
} }
if (order.status !== 'processing' && order.status !== 'pending_payment') {
if (['shipped', 'delivered'].includes(order.status)) { if (['shipped', 'delivered'].includes(order.status)) {
throw new BadRequestException('این سفارش قبلاً تکمیل و ارسال شده است'); throw new BadRequestException('این سفارش قبلاً تکمیل و ارسال شده است');
} }
}
const amountTomans = Number(order.totalAmount); const amountTomans = Number(order.totalAmount);
const amountRials = Math.round(amountTomans * 10); const amountRials = Math.round(amountTomans * 10);
if (amountRials < 1000) { if (amountRials < 1000) {
throw new BadRequestException( throw new BadRequestException('مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است');
'مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است',
);
} }
const frontendUrl = await this.zibalService.getFrontendUrl(); const frontendUrl = await this.zibalService.getFrontendUrl();
// Default callback points to frontend verify page (or backend proxy)
const callbackUrl = const callbackUrl =
customCallbackUrl || `${frontendUrl}/payment/verify?orderId=${order.id}`; customCallbackUrl || `${frontendUrl}/payment/verify?orderId=${order.id}`;
@ -111,11 +111,9 @@ export class PaymentService {
orderId: order.id, orderId: order.id,
amount: amountTomans, amount: amountTomans,
}; };
} catch (err: unknown) { } catch (err: any) {
const msg = err instanceof Error ? err.message : String(err); this.logger.error(`Error requesting payment from Zibal: ${err.message}`, err.stack);
const stack = err instanceof Error ? err.stack : undefined; throw new BadRequestException(err.message || 'خطا در اتصال به درگاه پرداخت');
this.logger.error(`Error requesting payment from Zibal: ${msg}`, stack);
throw new BadRequestException(msg || 'خطا در اتصال به درگاه پرداخت');
} }
} }
@ -191,15 +189,14 @@ export class PaymentService {
paymentUrl: this.zibalService.getStartUrl(trackId), paymentUrl: this.zibalService.getStartUrl(trackId),
amount: amountTomans, amount: amountTomans,
}; };
} catch (err: unknown) { } catch (err: any) {
const msg = err instanceof Error ? err.message : String(err); this.logger.error(`Error requesting wallet topup from Zibal: ${err.message}`);
this.logger.error(`Error requesting wallet topup from Zibal: ${msg}`); throw new BadRequestException(err.message || 'خطا در ارتباط با درگاه بانکی');
throw new BadRequestException(msg || 'خطا در ارتباط با درگاه بانکی');
} }
} }
/** /**
* 3. Verify & Process Callback from Zibal (With Idempotency & Amount Match Check) * 3. Verify & Process Callback from Zibal
*/ */
async verifyAndProcess( async verifyAndProcess(
trackId: string, trackId: string,
@ -215,42 +212,34 @@ export class PaymentService {
where: { trackId }, where: { trackId },
include: { include: {
order: { order: {
include: { include: { user: true, orderItems: { include: { product: true } } },
user: true,
orderItems: { include: { product: true } },
},
}, },
user: true, user: true,
}, },
}); });
if (!transaction && orderIdParam) { if (!transaction) {
// If transaction not found by trackId, check if orderId exists
if (orderIdParam) {
transaction = await this.prisma.paymentTransaction.findFirst({ transaction = await this.prisma.paymentTransaction.findFirst({
where: { orderId: orderIdParam }, where: { orderId: orderIdParam },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: { include: {
order: { order: {
include: { include: { user: true, orderItems: { include: { product: true } } },
user: true,
orderItems: { include: { product: true } },
},
}, },
user: true, user: true,
}, },
}); });
} }
}
if (!transaction) { if (!transaction) {
throw new NotFoundException('تراکنش پرداخت در سیستم یافت نشد'); throw new NotFoundException('تراکنش پرداخت در سیستم یافت نشد');
} }
// --- IDEMPOTENCY CHECK --- // If transaction is already verified, return successful details immediately
// If transaction is already marked as VERIFIED, return stored confirmation immediately
// without executing any duplicate side-effects (wallet increment, charity, status change, SMS).
if (transaction.status === 'VERIFIED') { if (transaction.status === 'VERIFIED') {
this.logger.log(
`Idempotency Guard: Transaction ${transaction.id} (trackId=${trackId}) is already VERIFIED. Skipping duplicate execution.`,
);
return { return {
success: true, success: true,
alreadyVerified: true, alreadyVerified: true,
@ -260,16 +249,13 @@ export class PaymentService {
cardNumber: transaction.cardNumber, cardNumber: transaction.cardNumber,
amount: Number(transaction.amount), amount: Number(transaction.amount),
orderId: transaction.orderId, orderId: transaction.orderId,
type: transaction.type,
message: 'تراکنش قبلاً با موفقیت تایید و پردازش شده است', message: 'تراکنش قبلاً با موفقیت تایید و پردازش شده است',
}; };
} }
// If user cancelled or Zibal signaled failure at the gate // If user cancelled or Zibal signaled failure at the gate
if (successParam === '0' || statusParam === '3') { if (successParam === '0' || statusParam === '3') {
const statusMessage = this.zibalService.getStatusMessage( const statusMessage = this.zibalService.getStatusMessage(statusParam || 3);
statusParam || 3,
);
await this.prisma.paymentTransaction.update({ await this.prisma.paymentTransaction.update({
where: { id: transaction.id }, where: { id: transaction.id },
data: { data: {
@ -314,53 +300,15 @@ export class PaymentService {
}; };
} }
// --- AMOUNT MATCH VERIFICATION ---
const expectedAmountRials = Number(
transaction.amountRials ||
Math.round(Number(transaction.amount) * 10),
);
if (
verifyRes.amount &&
Number(verifyRes.amount) > 0 &&
Number(verifyRes.amount) !== expectedAmountRials
) {
this.logger.error(
`Security Alert: Amount mismatch on trackId=${trackId}! Zibal reported ${verifyRes.amount} Rials, but expected ${expectedAmountRials} Rials in DB.`,
);
await this.prisma.paymentTransaction.update({
where: { id: transaction.id },
data: {
status: 'FAILED',
resultCode: verifyRes.result,
message: `مغایرت مالی: مبلغ تایید شده زیبال (${verifyRes.amount} ریال) با مبلغ سیستم (${expectedAmountRials} ریال) مطابقت ندارد.`,
},
});
return {
success: false,
status: 'FAILED',
trackId,
message: 'خطای امنیتی: مغایرت در مبلغ پرداخت شده با سفارش.',
orderId: transaction.orderId,
};
}
// SUCCESSFUL PAYMENT! // SUCCESSFUL PAYMENT!
const refNumber = verifyRes.refNumber const refNumber = verifyRes.refNumber ? String(verifyRes.refNumber) : undefined;
? String(verifyRes.refNumber)
: undefined;
const cardNumber = verifyRes.cardNumber || undefined; const cardNumber = verifyRes.cardNumber || undefined;
const paidDate = verifyRes.paidAt const paidDate = verifyRes.paidAt ? new Date(verifyRes.paidAt) : new Date();
? new Date(verifyRes.paidAt)
: new Date();
// Execute atomic status update with concurrency guard
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
// Optimistic lock: only update if still PENDING (prevents race condition) // 1. Mark transaction as VERIFIED
const updatedCount = await tx.paymentTransaction.updateMany({ await tx.paymentTransaction.update({
where: { id: transaction.id, status: 'PENDING' }, where: { id: transaction.id },
data: { data: {
status: 'VERIFIED', status: 'VERIFIED',
resultCode: verifyRes.result, resultCode: verifyRes.result,
@ -371,14 +319,6 @@ export class PaymentService {
}, },
}); });
// If another concurrent thread verified it first, exit safely
if (updatedCount.count === 0) {
this.logger.warn(
`Concurrent execution detected for transaction ${transaction.id}. Handled safely.`,
);
return;
}
// 2. If it's an Order payment // 2. If it's an Order payment
if (transaction.type === 'ORDER' && transaction.orderId) { if (transaction.type === 'ORDER' && transaction.orderId) {
await tx.order.update({ await tx.order.update({
@ -390,10 +330,7 @@ export class PaymentService {
}); });
// If charity donation was made, update user's total charity // If charity donation was made, update user's total charity
if ( if (transaction.order && Number(transaction.order.charityDonation) > 0) {
transaction.order &&
Number(transaction.order.charityDonation) > 0
) {
await tx.user.update({ await tx.user.update({
where: { id: transaction.userId }, where: { id: transaction.userId },
data: { data: {
@ -432,11 +369,8 @@ export class PaymentService {
// Send SMS notification if order payment // Send SMS notification if order payment
if (transaction.type === 'ORDER' && transaction.order?.user?.mobile) { if (transaction.type === 'ORDER' && transaction.order?.user?.mobile) {
const mobile = transaction.order.user.mobile; const mobile = transaction.order.user.mobile;
const trackingNum = const trackingNum = transaction.order.trackingNumber || transaction.order.id;
transaction.order.trackingNumber || transaction.order.id; const formattedAmount = Number(transaction.amount).toLocaleString('fa-IR');
const formattedAmount = Number(transaction.amount).toLocaleString(
'fa-IR',
);
this.smsService this.smsService
.sendOrderConfirmation(mobile, trackingNum, formattedAmount) .sendOrderConfirmation(mobile, trackingNum, formattedAmount)
@ -456,92 +390,20 @@ export class PaymentService {
type: transaction.type, type: transaction.type,
message: 'پرداخت با موفقیت انجام شد', message: 'پرداخت با موفقیت انجام شد',
}; };
} catch (err: unknown) { } catch (err: any) {
const msg = err instanceof Error ? err.message : String(err); this.logger.error(`Error during verify: ${err.message}`, err.stack);
const stack = err instanceof Error ? err.stack : undefined;
this.logger.error(`Error during verify: ${msg}`, stack);
return { return {
success: false, success: false,
status: 'ERROR', status: 'ERROR',
trackId, trackId,
message: msg || 'خطا در فرایند تایید تراکنش', message: err.message || 'خطا در فرایند تایید تراکنش',
orderId: transaction.orderId, orderId: transaction.orderId,
}; };
} }
} }
/** /**
* 4. Auto-Reconciliation / Cron Job for Pending Transactions (استعلام تراکنش‌های بلاتکلیف) * 4. Get Payment Transaction Details
*/
async reconcilePendingTransactions() {
this.logger.log('Starting pending transactions reconciliation job...');
// Find transactions that are PENDING and created between 5 minutes and 24 hours ago
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const pendingTransactions = await this.prisma.paymentTransaction.findMany({
where: {
status: 'PENDING',
trackId: { not: null },
createdAt: {
gte: oneDayAgo,
lte: fiveMinutesAgo,
},
},
include: { order: true, user: true },
take: 50,
});
const results = {
total: pendingTransactions.length,
verified: 0,
failed: 0,
remainedPending: 0,
errors: 0,
};
for (const tx of pendingTransactions) {
if (!tx.trackId) continue;
try {
const inquiry = await this.zibalService.inquiryPayment(tx.trackId);
this.logger.log(
`Inquiry for trackId=${tx.trackId}: status=${inquiry.status}, result=${inquiry.result}`,
);
// Status 1 (paid & verified) or 2 (paid, unverified)
if (inquiry.status === 1 || inquiry.status === 2 || inquiry.result === 100) {
await this.verifyAndProcess(tx.trackId, '1', String(inquiry.status), tx.orderId || undefined);
results.verified++;
} else if (inquiry.status === 3 || inquiry.status === -2 || inquiry.result === 202) {
// Cancelled or failed
const msg = this.zibalService.getStatusMessage(inquiry.status || 3);
await this.prisma.paymentTransaction.update({
where: { id: tx.id },
data: {
status: 'FAILED',
resultCode: inquiry.result,
message: msg,
},
});
results.failed++;
} else {
results.remainedPending++;
}
} catch (err: unknown) {
results.errors++;
const msg = err instanceof Error ? err.message : String(err);
this.logger.warn(`Failed inquiry for trackId=${tx.trackId}: ${msg}`);
}
}
this.logger.log(`Reconciliation finished: ${JSON.stringify(results)}`);
return results;
}
/**
* 5. Get Payment Transaction Details by TrackID
*/ */
async getTransaction(trackId: string) { async getTransaction(trackId: string) {
const transaction = await this.prisma.paymentTransaction.findUnique({ const transaction = await this.prisma.paymentTransaction.findUnique({
@ -554,7 +416,6 @@ export class PaymentService {
}, },
}, },
}, },
user: true,
}, },
}); });
@ -575,215 +436,6 @@ export class PaymentService {
paidAt: transaction.paidAt, paidAt: transaction.paidAt,
createdAt: transaction.createdAt, createdAt: transaction.createdAt,
order: transaction.order, order: transaction.order,
user: transaction.user ? {
id: transaction.user.id,
firstName: transaction.user.firstName,
lastName: transaction.user.lastName,
mobile: transaction.user.mobile,
} : null,
};
}
/**
* 6. Admin: Get List of Transactions with Advanced Search & Filter
*/
async getAdminTransactions(filters: AdminTransactionFilterDto) {
const {
page = 1,
limit = 15,
search,
status,
gateway,
type,
startDate,
endDate,
minAmount,
maxAmount,
sortBy = 'createdAt',
sortOrder = 'desc',
} = filters;
const skip = (page - 1) * limit;
const where: Prisma.PaymentTransactionWhereInput = {};
if (status) {
where.status = status;
}
if (gateway) {
where.gateway = gateway;
}
if (type) {
where.type = type;
}
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt.gte = new Date(startDate);
if (endDate) where.createdAt.lte = new Date(endDate);
}
if (minAmount !== undefined || maxAmount !== undefined) {
where.amount = {};
if (minAmount !== undefined) where.amount.gte = new Prisma.Decimal(minAmount);
if (maxAmount !== undefined) where.amount.lte = new Prisma.Decimal(maxAmount);
}
if (search && search.trim() !== '') {
const s = search.trim();
where.OR = [
{ trackId: { contains: s, mode: 'insensitive' } },
{ refNumber: { contains: s, mode: 'insensitive' } },
{ cardNumber: { contains: s, mode: 'insensitive' } },
{ description: { contains: s, mode: 'insensitive' } },
{
user: {
OR: [
{ firstName: { contains: s, mode: 'insensitive' } },
{ lastName: { contains: s, mode: 'insensitive' } },
{ mobile: { contains: s, mode: 'insensitive' } },
],
},
},
{
order: {
trackingNumber: { contains: s, mode: 'insensitive' },
},
},
];
}
const [data, total] = await Promise.all([
this.prisma.paymentTransaction.findMany({
where,
skip,
take: limit,
orderBy: { [sortBy]: sortOrder },
include: {
user: {
select: {
id: true,
firstName: true,
lastName: true,
mobile: true,
email: true,
},
},
order: {
select: {
id: true,
trackingNumber: true,
totalAmount: true,
status: true,
paymentMethod: true,
},
},
},
}),
this.prisma.paymentTransaction.count({ where }),
]);
return {
data,
meta: {
total,
page,
lastPage: Math.ceil(total / limit),
limit,
},
};
}
/**
* 7. Admin: Get Transaction Statistics
*/
async getAdminTransactionStats() {
const [
totalCount,
verifiedCount,
pendingCount,
failedCount,
verifiedSum,
todayVerified,
] = await Promise.all([
this.prisma.paymentTransaction.count(),
this.prisma.paymentTransaction.count({ where: { status: 'VERIFIED' } }),
this.prisma.paymentTransaction.count({ where: { status: 'PENDING' } }),
this.prisma.paymentTransaction.count({ where: { status: 'FAILED' } }),
this.prisma.paymentTransaction.aggregate({
_sum: { amount: true },
where: { status: 'VERIFIED' },
}),
this.prisma.paymentTransaction.aggregate({
_sum: { amount: true },
where: {
status: 'VERIFIED',
createdAt: {
gte: new Date(new Date().setHours(0, 0, 0, 0)),
},
},
}),
]);
const totalVolume = Number(verifiedSum._sum.amount || 0);
const todayVolume = Number(todayVerified._sum.amount || 0);
const successRate =
totalCount > 0 ? Math.round((verifiedCount / totalCount) * 100) : 0;
return {
totalCount,
verifiedCount,
pendingCount,
failedCount,
totalVolume,
todayVolume,
successRate,
};
}
/**
* 8. Admin: Perform Live Instant Inquiry on Zibal for a Transaction
*/
async adminLiveInquiry(transactionId: string) {
const transaction = await this.prisma.paymentTransaction.findUnique({
where: { id: transactionId },
include: { order: true, user: true },
});
if (!transaction) {
throw new NotFoundException('تراکنش یافت نشد');
}
if (!transaction.trackId) {
throw new BadRequestException('این تراکنش فاقد TrackId زیبال است');
}
const inquiryRes = await this.zibalService.inquiryPayment(transaction.trackId);
const statusMsg = this.zibalService.getStatusMessage(inquiryRes.status);
const resultMsg = this.zibalService.getResultMessage(inquiryRes.result);
// If transaction was PENDING and inquiry confirms payment, sync automatically
if (
transaction.status === 'PENDING' &&
(inquiryRes.status === 1 || inquiryRes.status === 2 || inquiryRes.result === 100)
) {
await this.verifyAndProcess(
transaction.trackId,
'1',
String(inquiryRes.status),
transaction.orderId || undefined,
);
}
return {
transactionId: transaction.id,
trackId: transaction.trackId,
currentDbStatus: transaction.status,
gatewayResponse: inquiryRes,
statusMessage: statusMsg,
resultMessage: resultMsg,
inquiredAt: new Date(),
}; };
} }
} }

View File

@ -115,12 +115,8 @@ export class ZibalService {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
this.logger.error( this.logger.error(`Zibal request HTTP error ${response.status}: ${errorText}`);
`Zibal request HTTP error ${response.status}: ${errorText}`, throw new Error(`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`);
);
throw new Error(
`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`,
);
} }
const data = (await response.json()) as ZibalRequestResponse; const data = (await response.json()) as ZibalRequestResponse;
@ -146,9 +142,7 @@ export class ZibalService {
trackId: String(trackId), trackId: String(trackId),
}; };
this.logger.log( this.logger.log(`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`);
`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`,
);
const response = await fetch(`${this.baseUrl}/v1/verify`, { const response = await fetch(`${this.baseUrl}/v1/verify`, {
method: 'POST', method: 'POST',
@ -160,12 +154,8 @@ export class ZibalService {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
this.logger.error( this.logger.error(`Zibal verify HTTP error ${response.status}: ${errorText}`);
`Zibal verify HTTP error ${response.status}: ${errorText}`, throw new Error(`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`);
);
throw new Error(
`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`,
);
} }
const data = (await response.json()) as ZibalVerifyResponse; const data = (await response.json()) as ZibalVerifyResponse;
@ -176,9 +166,7 @@ export class ZibalService {
/** /**
* 4. Inquiry Payment (استعلام وضعیت تراکنش) * 4. Inquiry Payment (استعلام وضعیت تراکنش)
*/ */
async inquiryPayment( async inquiryPayment(trackId: string | number): Promise<ZibalInquiryResponse> {
trackId: string | number,
): Promise<ZibalInquiryResponse> {
const merchant = await this.getMerchant(); const merchant = await this.getMerchant();
const payload = { const payload = {
@ -198,12 +186,8 @@ export class ZibalService {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
this.logger.error( this.logger.error(`Zibal inquiry HTTP error ${response.status}: ${errorText}`);
`Zibal inquiry HTTP error ${response.status}: ${errorText}`, throw new Error(`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`);
);
throw new Error(
`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`,
);
} }
const data = (await response.json()) as ZibalInquiryResponse; const data = (await response.json()) as ZibalInquiryResponse;

View File

@ -1,18 +1,15 @@
import { import {
Controller, Controller,
Get, Get,
Post,
Patch, Patch,
Put, Put,
Delete, Delete,
Body, Body,
Param, Param,
Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { SettingsService, ScientificTermData } from './settings.service'; import { SettingsService, ScientificTermData } from './settings.service';
import { SmsLogQuery } from '../common/services/sms.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../common/guards/roles.guard'; import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator'; import { Roles } from '../common/decorators/roles.decorator';
@ -23,7 +20,7 @@ import {
ApiOkResponse, ApiOkResponse,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی، سیستم و پیامک') @ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی و سیستم')
@Controller('settings') @Controller('settings')
export class SettingsController { export class SettingsController {
constructor(private readonly settingsService: SettingsService) {} constructor(private readonly settingsService: SettingsService) {}
@ -128,101 +125,4 @@ export class SettingsController {
updateSystemSettings(@Body() body: Prisma.InputJsonValue) { updateSystemSettings(@Body() body: Prisma.InputJsonValue) {
return this.settingsService.updateCategorySetting('system', body); return this.settingsService.updateCategorySetting('system', body);
} }
// SMS Gateway Settings
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('sms')
@ApiOperation({ summary: 'دریافت تنظیمات درگاه پیامک و کدهای پترن' })
getSmsSettings() {
return this.settingsService.getSmsSettings();
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Patch('sms')
@ApiOperation({ summary: 'به‌روزرسانی تنظیمات درگاه پیامک و کدهای پترن' })
updateSmsSettings(@Body() body: Prisma.InputJsonValue) {
return this.settingsService.updateSmsSettings(body);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Post('sms/test')
@ApiOperation({ summary: 'ارسال پیامک آزمایشی پترن به شماره دلخواه' })
testSms(
@Body('phone') phone: string,
@Body('patternId') patternId?: number,
@Body('args') args?: string[],
) {
return this.settingsService.testSms(phone, patternId, args);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('sms/patterns')
@ApiOperation({
summary: 'دریافت لیست تمامی پترن‌های تعریف شده در ملی پیامک',
})
getPatterns() {
return this.settingsService.getPatterns();
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Post('sms/patterns')
@ApiOperation({ summary: 'درج الگوی جدید در سامانه ملی پیامک' })
addPattern(
@Body('title') title: string,
@Body('body') body: string,
@Body('blackListId') blackListId?: number,
) {
return this.settingsService.addPattern(title, body, blackListId);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Put('sms/patterns/:bodyId')
@ApiOperation({
summary: 'ویرایش الگوی رد شده یا در حال ویرایش در ملی پیامک',
})
editPattern(@Param('bodyId') bodyId: string, @Body('body') body: string) {
return this.settingsService.editPattern(Number(bodyId), body);
}
// SMS Logs & Tracking Endpoints
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Get('sms/logs')
@ApiOperation({
summary: 'دریافت گزارشات و لاگ‌های کامل پیامک‌های ارسالی با فیلتر و جستجو',
})
getSmsLogs(@Query() query: SmsLogQuery) {
return this.settingsService.getSmsLogs(query);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Delete('sms/logs/:id')
@ApiOperation({ summary: 'حذف یک رکورد لاگ پیامک' })
deleteSmsLog(@Param('id') id: string) {
return this.settingsService.deleteSmsLog(id);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('Admin')
@ApiBearerAuth()
@Delete('sms/logs')
@ApiOperation({ summary: 'پاکسازی تمامی لاگ‌های پیامک' })
clearAllSmsLogs() {
return this.settingsService.clearAllSmsLogs();
}
} }

View File

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { SmsService, SmsLogQuery } from '../common/services/sms.service';
export class ScientificTermData { export class ScientificTermData {
term?: string; term?: string;
@ -11,10 +10,7 @@ export class ScientificTermData {
@Injectable() @Injectable()
export class SettingsService { export class SettingsService {
constructor( constructor(private prisma: PrismaService) {}
private prisma: PrismaService,
private smsService: SmsService,
) {}
async getUiTexts() { async getUiTexts() {
const [uiTexts, settings] = await Promise.all([ const [uiTexts, settings] = await Promise.all([
@ -86,45 +82,4 @@ export class SettingsService {
create: { key, category, value }, create: { key, category, value },
}); });
} }
async getSmsSettings() {
return this.smsService.getSmsConfig();
}
async updateSmsSettings(value: Prisma.InputJsonValue) {
const key = 'sms_config';
return this.prisma.setting.upsert({
where: { key },
update: { category: 'sms', value },
create: { key, category: 'sms', value },
});
}
async testSms(targetPhone: string, patternId?: number, args?: string[]) {
return this.smsService.testSms(targetPhone, patternId, args);
}
async getPatterns() {
return this.smsService.getPatterns();
}
async addPattern(title: string, body: string, blackListId?: number) {
return this.smsService.addPattern(title, body, blackListId);
}
async editPattern(bodyId: number, body: string) {
return this.smsService.editPattern(bodyId, body);
}
async getSmsLogs(query: SmsLogQuery) {
return this.smsService.getSmsLogs(query);
}
async deleteSmsLog(id: string) {
return this.smsService.deleteSmsLog(id);
}
async clearAllSmsLogs() {
return this.smsService.clearAllSmsLogs();
}
} }

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders, MessageSquare, Receipt, CreditCard } from 'lucide-react'; import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders } from 'lucide-react';
import api from '../services/api'; import api from '../services/api';
import { useAdminAuthStore } from '../store/adminAuthStore'; import { useAdminAuthStore } from '../store/adminAuthStore';
@ -16,7 +16,6 @@ const menuGroups = [
title: 'فروشگاه و تخصصی', title: 'فروشگاه و تخصصی',
items: [ items: [
{ icon: ShoppingCart, label: 'سفارشات', path: '/orders' }, { icon: ShoppingCart, label: 'سفارشات', path: '/orders' },
{ icon: Receipt, label: 'تراکنش‌ها و لاگ پرداخت', path: '/transactions' },
{ icon: Package, label: 'محصولات', path: '/products' }, { icon: Package, label: 'محصولات', path: '/products' },
{ icon: FolderTree, label: 'دسته‌بندی‌ها', path: '/categories' }, { icon: FolderTree, label: 'دسته‌بندی‌ها', path: '/categories' },
{ icon: Tag, label: 'کدهای تخفیف', path: '/coupons' }, { icon: Tag, label: 'کدهای تخفیف', path: '/coupons' },
@ -48,7 +47,6 @@ const menuGroups = [
title: 'سیستم و تنظیمات', title: 'سیستم و تنظیمات',
items: [ items: [
{ icon: Settings, label: 'تنظیمات کلی', path: '/settings' }, { icon: Settings, label: 'تنظیمات کلی', path: '/settings' },
{ icon: MessageSquare, label: 'تنظیمات پیامک', path: '/settings/sms' },
{ icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' }, { icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' },
{ icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' }, { icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' },
{ icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' }, { icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' },

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2 } from 'lucide-react';
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2, MessageSquare } from 'lucide-react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
@ -107,15 +106,6 @@ export default function Settings() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت پارامترهای اصلی و سیستمی کانینا</p> <p className="text-gray-500 font-medium mt-1">مدیریت پارامترهای اصلی و سیستمی کانینا</p>
</div> </div>
<div className="flex items-center gap-2 flex-wrap">
<Link
to="/settings/sms"
className="bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 text-xs font-bold px-4 py-2.5 rounded-xl transition-all flex items-center gap-2"
>
<MessageSquare className="w-4 h-4 text-purple-600" />
تنظیمات درگاه پیامک (MeliPayamak)
</Link>
</div>
</div> </div>
{isLoading ? ( {isLoading ? (

File diff suppressed because it is too large Load Diff

View File

@ -1,681 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
CreditCard,
Search,
RefreshCw,
Eye,
Copy,
CheckCircle2,
XCircle,
Clock,
Filter,
DollarSign,
TrendingUp,
AlertTriangle,
Receipt,
User,
ShoppingBag,
ArrowUpDown,
ExternalLink,
ShieldCheck,
RotateCcw
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
interface Transaction {
id: string;
userId: string;
orderId?: string | null;
amount: number | string;
amountRials?: string | number | null;
gateway: string;
trackId?: string | null;
refNumber?: string | null;
cardNumber?: string | null;
status: string;
resultCode?: number | null;
message?: string | null;
description?: string | null;
type: string;
paidAt?: string | null;
createdAt: string;
user?: {
id: string;
firstName: string;
lastName: string;
mobile: string;
email?: string;
} | null;
order?: {
id: string;
trackingNumber?: string;
totalAmount: number | string;
status: string;
paymentMethod?: string;
} | null;
}
interface Stats {
totalCount: number;
verifiedCount: number;
pendingCount: number;
failedCount: number;
totalVolume: number;
todayVolume: number;
successRate: number;
}
export default function Transactions() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isReconciling, setIsReconciling] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalItems, setTotalItems] = useState(0);
// Filters
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [gatewayFilter, setGatewayFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [sortBy, setSortBy] = useState('createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Details Modal
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
const [liveInquiryLoading, setLiveInquiryLoading] = useState(false);
const [liveInquiryData, setLiveInquiryData] = useState<any>(null);
const fetchStats = async () => {
try {
const res = await api.get('/payment/admin/stats');
if (res.data) {
setStats(res.data);
}
} catch (e) {
console.error('Failed to fetch stats', e);
}
};
const fetchTransactions = useCallback(async () => {
try {
setIsLoading(true);
const params = new URLSearchParams();
params.append('page', String(page));
params.append('limit', '15');
if (search) params.append('search', search);
if (statusFilter) params.append('status', statusFilter);
if (gatewayFilter) params.append('gateway', gatewayFilter);
if (typeFilter) params.append('type', typeFilter);
params.append('sortBy', sortBy);
params.append('sortOrder', sortOrder);
const res = await api.get(`/payment/admin/transactions?${params.toString()}`);
if (res.data) {
setTransactions(res.data.data || []);
setTotalPages(res.data.meta?.lastPage || 1);
setTotalItems(res.data.meta?.total || 0);
}
} catch (e) {
console.error('Failed to fetch transactions', e);
toast.error('خطا در بارگذاری لیست تراکنش‌ها');
} finally {
setIsLoading(false);
}
}, [page, search, statusFilter, gatewayFilter, typeFilter, sortBy, sortOrder]);
useEffect(() => {
fetchTransactions();
fetchStats();
}, [fetchTransactions]);
const handleCopy = (text: string, title: string) => {
navigator.clipboard.writeText(text);
toast.success(`${title} کپی شد!`);
};
const handleReconcile = async () => {
try {
setIsReconciling(true);
const res = await api.post('/payment/admin/reconcile');
toast.success(
`انطباق تراکنش‌ها انجام شد: ${res.data.verified} تایید شده، ${res.data.failed} ناموفق`,
);
fetchTransactions();
fetchStats();
} catch (e) {
console.error('Reconcile error', e);
toast.error('خطا در اجرای استعلام خودکار');
} finally {
setIsReconciling(false);
}
};
const handleLiveInquiry = async (txId: string) => {
try {
setLiveInquiryLoading(true);
const res = await api.get(`/payment/admin/live-inquiry/${txId}`);
setLiveInquiryData(res.data);
toast.success('استعلام زنده زیبال با موفقیت دریافت شد');
fetchTransactions();
fetchStats();
} catch (e: any) {
const msg = e.response?.data?.message || 'خطا در استعلام از درگاه زیبال';
toast.error(msg);
} finally {
setLiveInquiryLoading(false);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<Receipt className="w-7 h-7 text-purple-600" />
لاگ‌ها و مدیریت تراکنش‌های مالی
</h2>
<p className="text-gray-500 font-medium mt-1">
رهگیری لحظه‌ای پرداخت‌های آنلاین زیبال، کارت به کارت، کیف پول و عیب‌یابی تراکنش‌ها
</p>
</div>
<button
onClick={handleReconcile}
disabled={isReconciling}
className="bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 text-xs font-bold px-4 py-2.5 rounded-xl transition-all flex items-center gap-2 shadow-sm disabled:opacity-50"
>
<RotateCcw className={`w-4 h-4 ${isReconciling ? 'animate-spin' : ''}`} />
<span>{isReconciling ? 'در حال استعلام...' : 'انطباق خودکار تراکنش‌های معلق'}</span>
</button>
</div>
{/* Summary Stat Cards */}
{stats && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">حجم کل تراکنش‌های موفق</p>
<p className="text-xl font-black text-gray-900 mt-1">
{Number(stats.totalVolume).toLocaleString('fa-IR')}{' '}
<span className="text-xs font-normal text-gray-400">تومان</span>
</p>
</div>
<div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center">
<DollarSign className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">پرداخت‌های موفق امروز</p>
<p className="text-xl font-black text-emerald-600 mt-1">
{Number(stats.todayVolume).toLocaleString('fa-IR')}{' '}
<span className="text-xs font-normal text-gray-400">تومان</span>
</p>
</div>
<div className="w-12 h-12 bg-emerald-50 text-emerald-600 rounded-2xl flex items-center justify-center">
<TrendingUp className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">نرخ موفقیت پرداخت‌ها</p>
<p className="text-xl font-black text-indigo-600 mt-1">
٪{Number(stats.successRate).toLocaleString('fa-IR')}
</p>
</div>
<div className="w-12 h-12 bg-indigo-50 text-indigo-600 rounded-2xl flex items-center justify-center">
<ShieldCheck className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">وضعیت تراکنش‌ها</p>
<div className="flex items-center gap-3 mt-1 text-xs font-bold">
<span className="text-emerald-600">✓ {stats.verifiedCount}</span>
<span className="text-amber-500">⏳ {stats.pendingCount}</span>
<span className="text-rose-600">✗ {stats.failedCount}</span>
</div>
</div>
<div className="w-12 h-12 bg-gray-50 text-gray-600 rounded-2xl flex items-center justify-center">
<Receipt className="w-6 h-6" />
</div>
</div>
</div>
)}
{/* Filter and Search Bar */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
{/* Search Input */}
<div className="lg:col-span-2 relative">
<Search className="w-4 h-4 text-gray-400 absolute right-3.5 top-3.5" />
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
placeholder="جستجو بر اساس نام، موبایل، Track ID، شماره مرجع..."
className="w-full pl-3 pr-10 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
{/* Status Filter */}
<div>
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه وضعیت‌ها</option>
<option value="VERIFIED">موفق (VERIFIED)</option>
<option value="PENDING">در انتظار (PENDING)</option>
<option value="FAILED">ناموفق (FAILED)</option>
</select>
</div>
{/* Gateway Filter */}
<div>
<select
value={gatewayFilter}
onChange={(e) => {
setGatewayFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه درگاه‌ها</option>
<option value="zibal">درگاه زیبال (Zibal)</option>
<option value="wallet">کیف پول (Wallet)</option>
<option value="card">کارت به کارت</option>
</select>
</div>
{/* Type Filter */}
<div>
<select
value={typeFilter}
onChange={(e) => {
setTypeFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه انواع</option>
<option value="ORDER">پرداخت سفارش (ORDER)</option>
<option value="WALLET_TOPUP">شارژ کیف پول (TOPUP)</option>
</select>
</div>
</div>
{/* Total records found */}
<div className="flex items-center justify-between text-xs text-gray-500 font-bold pt-2 border-t border-gray-100">
<span>مجموع {totalItems.toLocaleString('fa-IR')} تراکنش ثبت شده</span>
<button
onClick={() => {
setSearch('');
setStatusFilter('');
setGatewayFilter('');
setTypeFilter('');
setPage(1);
}}
className="text-purple-600 hover:underline"
>
پاک کردن فیلترها
</button>
</div>
</div>
{/* Transactions Table */}
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden">
{isLoading ? (
<div className="p-12 flex justify-center">
<Spinner size="lg" className="text-purple-600" />
</div>
) : transactions.length === 0 ? (
<div className="p-12 text-center text-gray-500 font-bold">
هیچ تراکنشی با فیلترهای مشخص شده یافت نشد.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-500">
<th className="p-4">کاربر خریدار</th>
<th className="p-4">سفارش / نوع</th>
<th className="p-4">مبلغ (تومان)</th>
<th className="p-4">درگاه</th>
<th className="p-4">شناسه‌های پرداخت</th>
<th className="p-4">وضعیت</th>
<th className="p-4">تاریخ و زمان</th>
<th className="p-4 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 text-xs font-bold">
{transactions.map((tx) => {
const isVerified = tx.status === 'VERIFIED';
const isPending = tx.status === 'PENDING';
const isFailed = tx.status === 'FAILED';
return (
<tr key={tx.id} className="hover:bg-gray-50/60 transition-colors">
{/* User Info */}
<td className="p-4">
{tx.user ? (
<div>
<p className="font-black text-gray-900">
{tx.user.firstName} {tx.user.lastName}
</p>
<p className="text-[11px] text-gray-400 font-mono mt-0.5 dir-ltr text-right">
{tx.user.mobile}
</p>
</div>
) : (
<span className="text-gray-400">کاربر مهمان</span>
)}
</td>
{/* Order / Type */}
<td className="p-4">
{tx.type === 'WALLET_TOPUP' ? (
<span className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded-lg text-[10px] font-black">
شارژ کیف پول
</span>
) : (
<div>
<span className="text-purple-700 font-mono text-[11px] font-black dir-ltr block">
{tx.order?.trackingNumber || 'سفارش آنلاین'}
</span>
<span className="text-[10px] text-gray-400 font-medium">
خرید محصول
</span>
</div>
)}
</td>
{/* Amount */}
<td className="p-4 font-black text-gray-900 font-mono text-sm">
{Number(tx.amount).toLocaleString('fa-IR')}
</td>
{/* Gateway */}
<td className="p-4">
<span className="bg-indigo-50 text-indigo-700 px-2.5 py-1 rounded-lg text-[10px] font-black">
{tx.gateway === 'zibal' ? 'زیبال (Zibal)' : tx.gateway}
</span>
</td>
{/* Tracking Numbers */}
<td className="p-4 font-mono text-[11px]">
{tx.trackId && (
<div className="flex items-center gap-1 text-gray-800">
<span className="text-gray-400 text-[10px]">Track:</span>
<span className="dir-ltr">{tx.trackId}</span>
<button
type="button"
onClick={() => handleCopy(tx.trackId!, 'Track ID')}
className="text-gray-400 hover:text-purple-600"
>
<Copy className="w-3 h-3" />
</button>
</div>
)}
{tx.refNumber && (
<div className="flex items-center gap-1 text-emerald-700 mt-0.5">
<span className="text-gray-400 text-[10px]">Ref:</span>
<span className="dir-ltr">{tx.refNumber}</span>
<button
type="button"
onClick={() => handleCopy(tx.refNumber!, 'شماره مرجع')}
className="text-gray-400 hover:text-purple-600"
>
<Copy className="w-3 h-3" />
</button>
</div>
)}
</td>
{/* Status */}
<td className="p-4">
{isVerified && (
<span className="inline-flex items-center gap-1 bg-emerald-50 text-emerald-700 border border-emerald-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<CheckCircle2 className="w-3 h-3" />
موفق
</span>
)}
{isPending && (
<span className="inline-flex items-center gap-1 bg-amber-50 text-amber-700 border border-amber-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<Clock className="w-3 h-3" />
در انتظار
</span>
)}
{isFailed && (
<span className="inline-flex items-center gap-1 bg-rose-50 text-rose-700 border border-rose-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<XCircle className="w-3 h-3" />
ناموفق
</span>
)}
</td>
{/* Date */}
<td className="p-4 text-[11px] text-gray-500">
{new Date(tx.createdAt).toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</td>
{/* Actions */}
<td className="p-4 text-center">
<div className="flex items-center justify-center gap-1.5">
<button
onClick={() => {
setSelectedTx(tx);
setLiveInquiryData(null);
}}
className="p-2 bg-gray-100 hover:bg-purple-50 text-gray-600 hover:text-purple-600 rounded-xl transition-all"
title="مشاهده جزئیات و عیب‌یابی"
>
<Eye className="w-4 h-4" />
</button>
{tx.gateway === 'zibal' && tx.trackId && (
<button
onClick={() => {
setSelectedTx(tx);
handleLiveInquiry(tx.id);
}}
className="p-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-xl transition-all"
title="استعلام زنده از زیبال"
>
<RotateCcw className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* Pagination */}
<div className="p-4 border-t border-gray-100">
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={(p) => setPage(p)}
/>
</div>
</div>
{/* Diagnostics and Details Modal */}
{selectedTx && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
<Receipt className="w-5 h-5 text-purple-600" />
جزئیات تراکنش و ریشه‌یابی خطا
</h3>
<button
onClick={() => {
setSelectedTx(null);
setLiveInquiryData(null);
}}
className="text-gray-400 hover:text-gray-700 text-xl font-bold"
>
✕
</button>
</div>
{/* Quick Grid Info */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 text-xs bg-gray-50 p-4 rounded-2xl border border-gray-200">
<div>
<span className="text-gray-400 block font-medium">وضعیت تراکنش:</span>
<span
className={`font-black mt-0.5 inline-block ${
selectedTx.status === 'VERIFIED'
? 'text-emerald-600'
: selectedTx.status === 'PENDING'
? 'text-amber-500'
: 'text-rose-600'
}`}
>
{selectedTx.status}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">مبلغ تراکنش:</span>
<span className="font-black text-gray-900 mt-0.5 inline-block">
{Number(selectedTx.amount).toLocaleString('fa-IR')} تومان
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">درگاه پرداخت:</span>
<span className="font-black text-gray-900 mt-0.5 inline-block">
{selectedTx.gateway}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شناسه پیگیری (Track ID):</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.trackId || 'ثبت نشده'}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شماره مرجع بانکی:</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.refNumber || 'ثبت نشده'}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شماره کارت ماسک شده:</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.cardNumber || 'ثبت نشده'}
</span>
</div>
</div>
{/* Error Message / Reason */}
{selectedTx.message && (
<div
className={`p-4 rounded-2xl border ${
selectedTx.status === 'VERIFIED'
? 'bg-emerald-50 border-emerald-200 text-emerald-900'
: 'bg-rose-50 border-rose-200 text-rose-900'
}`}
>
<div className="flex items-center gap-2 font-black text-xs mb-1">
{selectedTx.status === 'VERIFIED' ? (
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
) : (
<AlertTriangle className="w-4 h-4 text-rose-600" />
)}
<span>پیام و نتیجه گزارش درگاه:</span>
</div>
<p className="text-xs font-bold leading-relaxed">{selectedTx.message}</p>
{selectedTx.resultCode && (
<p className="text-[11px] font-mono mt-1 text-gray-500">
کد نتیجه زیبال: {selectedTx.resultCode}
</p>
)}
</div>
)}
{/* Live Inquiry Section */}
<div className="border border-indigo-100 bg-indigo-50/50 p-5 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RotateCcw className="w-4 h-4 text-indigo-600" />
<h4 className="font-black text-xs text-indigo-900">
استعلام زنده از سرور زیبال (Live Gateway Inquiry)
</h4>
</div>
<button
onClick={() => handleLiveInquiry(selectedTx.id)}
disabled={liveInquiryLoading}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-3 py-1.5 rounded-xl transition-all flex items-center gap-1.5 disabled:opacity-50"
>
<RefreshCw
className={`w-3.5 h-3.5 ${liveInquiryLoading ? 'animate-spin' : ''}`}
/>
<span>استعلام زنده لحظه‌ای</span>
</button>
</div>
{liveInquiryData && (
<div className="bg-white p-4 rounded-xl border border-indigo-200 space-y-2 text-xs font-mono">
<div className="flex justify-between text-gray-700 font-bold font-vazir">
<span>وضعیت زیبال:</span>
<span className="text-indigo-600">{liveInquiryData.statusMessage}</span>
</div>
<pre className="text-[11px] bg-slate-900 text-emerald-400 p-3 rounded-lg overflow-x-auto dir-ltr">
{JSON.stringify(liveInquiryData.gatewayResponse, null, 2)}
</pre>
</div>
)}
</div>
{/* Close Button */}
<div className="flex justify-end">
<button
onClick={() => {
setSelectedTx(null);
setLiveInquiryData(null);
}}
className="bg-gray-100 hover:bg-gray-200 text-gray-800 font-bold px-6 py-2.5 rounded-xl text-xs transition-colors"
>
بستن
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -33,8 +33,6 @@ const B2BManager = lazy(() => import('../pages/B2BManager'));
const SeoSettingsPage = lazy(() => import('../pages/SeoSettingsPage')); const SeoSettingsPage = lazy(() => import('../pages/SeoSettingsPage'));
const FinancialSettingsPage = lazy(() => import('../pages/FinancialSettingsPage')); const FinancialSettingsPage = lazy(() => import('../pages/FinancialSettingsPage'));
const SystemSettingsPage = lazy(() => import('../pages/SystemSettingsPage')); const SystemSettingsPage = lazy(() => import('../pages/SystemSettingsPage'));
const SmsSettingsPage = lazy(() => import('../pages/SmsSettingsPage'));
const Transactions = lazy(() => import('../pages/Transactions'));
export interface AdminRouteConfig { export interface AdminRouteConfig {
path: string; path: string;
@ -58,10 +56,8 @@ export const router = createBrowserRouter([
{ path: 'users/*', element: <Users /> }, { path: 'users/*', element: <Users /> },
{ path: 'products/*', element: <Products /> }, { path: 'products/*', element: <Products /> },
{ path: 'orders/*', element: <Orders /> }, { path: 'orders/*', element: <Orders /> },
{ path: 'transactions/*', element: <Transactions /> },
{ path: 'coupons/*', element: <Coupons /> }, { path: 'coupons/*', element: <Coupons /> },
{ path: 'settings', element: <Settings /> }, { path: 'settings', element: <Settings /> },
{ path: 'settings/sms', element: <SmsSettingsPage /> },
{ path: 'settings/seo', element: <SeoSettingsPage /> }, { path: 'settings/seo', element: <SeoSettingsPage /> },
{ path: 'settings/financial', element: <FinancialSettingsPage /> }, { path: 'settings/financial', element: <FinancialSettingsPage /> },
{ path: 'settings/system', element: <SystemSettingsPage /> }, { path: 'settings/system', element: <SystemSettingsPage /> },

View File

@ -1,10 +1,10 @@
{ {
"0": "AdminService", "0": "AdminService",
"1": "pets/pets.controller.ts", "1": "Pet Management Controller",
"2": "Roles", "2": "SettingsController",
"3": "UsersService", "3": "UsersService",
"4": "CmsController", "4": "CMS Content Management",
"5": "BannersService", "5": "OrdersService",
"6": "auth.controller.ts", "6": "auth.controller.ts",
"7": "app.module.ts", "7": "app.module.ts",
"8": "CreateVideoDto", "8": "CreateVideoDto",
@ -12,32 +12,32 @@
"10": "SmartAdvisor.tsx", "10": "SmartAdvisor.tsx",
"11": "compilerOptions", "11": "compilerOptions",
"12": "toPersian", "12": "toPersian",
"13": "SmsService", "13": "Media and Blog UI Components",
"14": "ProductsService", "14": "ProductsService",
"15": "lib/services/api.ts", "15": "lib/services/api.ts",
"16": "eslint", "16": "devDependencies",
"17": "useSettingsStore", "17": "useSettingsStore",
"18": "AppModule", "18": "AppModule",
"19": "B2B Inquiry Controller", "19": "B2B Inquiry Controller",
"20": "Category Management Controller", "20": "Category Management Controller",
"21": "auth.service.ts", "21": "Banner Management Controller",
"22": "MediaController", "22": "Media Upload Controller",
"23": "Ingredient Management Controller", "23": "Ingredient Management Controller",
"24": "adminRoutes.tsx", "24": "Admin Dashboard Components",
"25": "admin.module.ts", "25": "admin.module.ts",
"26": "ConfirmModal.tsx", "26": "Pet DTOs and Controller",
"27": "Prescription Review Controller", "27": "Prescription Review Controller",
"28": "Smart Advisor Controller", "28": "Smart Advisor Controller",
"29": "Testimonials Management Controller", "29": "Testimonials Management Controller",
"30": "src/services/api.ts", "30": "Admin Sidebar and Layout",
"31": "Spinner.tsx", "31": "Admin Settings Managers",
"32": "useCartStore", "32": "useCartStore",
"33": "ZibalService", "33": "ZibalService",
"34": "main.ts", "34": "main.ts",
"35": "ContactService", "35": "ContactService",
"36": "Backend TypeScript Config", "36": "Backend TypeScript Config",
"37": "App TypeScript Config", "37": "App TypeScript Config",
"38": "Coupons.tsx", "38": "CMS and Media Modals",
"39": "dependencies", "39": "dependencies",
"40": "Node TypeScript Config", "40": "Node TypeScript Config",
"41": "Admin Blog Controller", "41": "Admin Blog Controller",
@ -45,7 +45,7 @@
"43": "devDependencies", "43": "devDependencies",
"44": "Generic CRUD Controller", "44": "Generic CRUD Controller",
"45": "seo.module.ts", "45": "seo.module.ts",
"46": "OrdersController", "46": "Coupon Management UI",
"47": "Project Build Scripts", "47": "Project Build Scripts",
"48": "Home Data Module", "48": "Home Data Module",
"49": "devDependencies", "49": "devDependencies",
@ -61,19 +61,19 @@
"59": "PrismaService", "59": "PrismaService",
"60": "Jest Testing Config", "60": "Jest Testing Config",
"61": "PaginationDto", "61": "PaginationDto",
"62": "SettingsService", "62": "BlogsController",
"63": "FE-001", "63": "FE-001",
"64": "payment.module.ts", "64": "pagination.dto.ts",
"65": "rules/graphify.md", "65": "rules/graphify.md",
"66": "App Health Controller", "66": "App Health Controller",
"67": "dependencies", "67": "dependencies",
"68": "OrdersService", "68": "Pet Business Logic",
"69": "Integrity Validation Scripts", "69": "Integrity Validation Scripts",
"70": "Admin Panel Package Config", "70": "Admin Panel Package Config",
"71": "Analytics and Report Charts", "71": "Analytics and Report Charts",
"72": "Task Orchestration Scripts", "72": "Task Orchestration Scripts",
"73": "Backend Package Config", "73": "Backend Package Config",
"74": "BlogsController", "74": "Health Log DTOs",
"75": "React Error Boundary", "75": "React Error Boundary",
"76": "Application Package Config", "76": "Application Package Config",
"77": "WikiController", "77": "WikiController",
@ -92,7 +92,7 @@
"90": "Blog Listing Page", "90": "Blog Listing Page",
"91": "Blog Post Detail Page", "91": "Blog Post Detail Page",
"92": "@types/node", "92": "@types/node",
"93": "devDependencies", "93": "typescript",
"94": "UI Text Seeding", "94": "UI Text Seeding",
"95": "Wiki Terms Seeding", "95": "Wiki Terms Seeding",
"96": "Blog Management DTOs", "96": "Blog Management DTOs",
@ -102,7 +102,7 @@
"100": "Network Status Banner", "100": "Network Status Banner",
"101": "Docker Deployment Scripts", "101": "Docker Deployment Scripts",
"102": "DB-001", "102": "DB-001",
"103": "BlogsService", "103": "typescript-eslint",
"104": "Database Migration Scripts", "104": "Database Migration Scripts",
"105": "Scientific Terms Schema", "105": "Scientific Terms Schema",
"106": "Blog Data Seeding", "106": "Blog Data Seeding",
@ -116,10 +116,10 @@
"114": "Manifest Data Generation", "114": "Manifest Data Generation",
"115": "Honest Manifest Synchronization", "115": "Honest Manifest Synchronization",
"116": "Manifest Entry Synchronization", "116": "Manifest Entry Synchronization",
"117": "WikiService", "117": "Pet Reminder DTOs",
"118": "TS-001", "118": "TS-001",
"119": "TEST-001", "119": "TEST-001",
"120": "ValidateCouponDto", "120": "@tailwindcss/postcss",
"121": "@types/react-dom", "121": "@types/react-dom",
"122": "Admin Panel TSConfig", "122": "Admin Panel TSConfig",
"123": "About Page Component", "123": "About Page Component",
@ -129,19 +129,19 @@
"127": "DEVOPS-001", "127": "DEVOPS-001",
"128": "DOC-001", "128": "DOC-001",
"129": "What You Must Do When Invoked", "129": "What You Must Do When Invoked",
"130": "JwtAuthGuard", "130": "Roles",
"131": "راهنمای تست سیستم (Software Testing)", "131": "راهنمای تست سیستم (Software Testing)",
"132": "Role & Core Objective", "132": "Role & Core Objective",
"133": "Required Review Group Closures", "133": "Required Review Group Closures",
"134": "Operational Rules & Boundaries", "134": "Operational Rules & Boundaries",
"135": "Operational Rules & Boundaries", "135": "Operational Rules & Boundaries",
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3", "136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"137": "@nestjs/cli", "137": "NestJS CLI Tooling",
"138": "Operational Rules & Boundaries", "138": "Operational Rules & Boundaries",
"139": "Operational Rules & Boundaries", "139": "Operational Rules & Boundaries",
"140": "Bcrypt Type Definitions", "140": "Bcrypt Type Definitions",
"141": "bcryptjs", "141": "Passport JWT Type Definitions",
"142": "helmet", "142": "Supertest Type Definitions",
"143": "Blog Entity Model", "143": "Blog Entity Model",
"144": "Home Entity Model", "144": "Home Entity Model",
"145": "Wiki Entity Model", "145": "Wiki Entity Model",
@ -237,7 +237,7 @@
"235": "👁️ UX & Persona Interface Review (08_visual_qa)", "235": "👁️ UX & Persona Interface Review (08_visual_qa)",
"236": "Compiler Diagnostic Dispositions", "236": "Compiler Diagnostic Dispositions",
"237": "@eslint/eslintrc", "237": "@eslint/eslintrc",
"238": "js-yaml", "238": "eslint-plugin-prettier",
"239": "globals", "239": "globals",
"240": "prettier", "240": "prettier",
"241": "prisma", "241": "prisma",
@ -263,25 +263,5 @@
"261": "shabnam-font-v5.0.1/CHANGELOG.md", "261": "shabnam-font-v5.0.1/CHANGELOG.md",
"262": "application/CLAUDE.md", "262": "application/CLAUDE.md",
"263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md", "263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"264": "tailwindcss", "264": "tailwindcss"
"265": "typescript-eslint",
"266": "@nestjs/core",
"267": "@nestjs/jwt",
"268": "@nestjs/swagger",
"269": "@nestjs/throttler",
"270": "passport-jwt",
"271": "@prisma/client",
"272": "swagger-ui-express",
"273": "AGENTS.md",
"274": "eslint-config-prettier",
"275": "@eslint/js",
"276": "jest",
"277": "@nestjs/schematics",
"278": "@nestjs/testing",
"279": "source-map-support",
"280": "ts-jest",
"281": "tsconfig-paths",
"282": "@types/bcryptjs",
"283": "typescript-eslint",
"284": "globals"
} }

File diff suppressed because one or more lines are too long

View File

@ -1,79 +1,79 @@
{ {
"0": "AdminService", "0": "AdminService",
"1": "PetsController", "1": "Pet Management Controller",
"2": "Roles", "2": "Roles",
"3": "UsersService", "3": "UsersService",
"4": "CmsController", "4": "CMS Content Management",
"5": "BannersService", "5": "OrdersController",
"6": "auth.service.ts", "6": "auth.controller.ts",
"7": "app.module.ts", "7": "app.module.ts",
"8": "CreateVideoDto", "8": "CreateVideoDto",
"9": "ProductService", "9": "Public Catalog Pages",
"10": "SmartAdvisor.tsx", "10": "Client Layout and Auth Modals",
"11": "compilerOptions", "11": "Prisma and TypeScript Config",
"12": "toPersian", "12": "Checkout and Dashboard Components",
"13": "pets/pets.controller.ts", "13": "Media and Blog UI Components",
"14": "ProductsService", "14": "ProductsService",
"15": "lib/services/api.ts", "15": "Contact and Prescription Features",
"16": "devDependencies", "16": "Development Tooling Config",
"17": "useSettingsStore", "17": "Home Page Client Components",
"18": "MetricsController", "18": "AppModule",
"19": "B2B Inquiry Controller", "19": "B2B Inquiry Controller",
"20": "Category Management Controller", "20": "Category Management Controller",
"21": "RedisService", "21": "Banner Management Controller",
"22": "MediaController", "22": "Media Upload Controller",
"23": "Ingredient Management Controller", "23": "Ingredient Management Controller",
"24": "adminRoutes.tsx", "24": "Admin Dashboard Components",
"25": "admin.module.ts", "25": "ReportsController",
"26": "api", "26": "Pet DTOs and Controller",
"27": "Prescription Review Controller", "27": "Prescription Review Controller",
"28": "Smart Advisor Controller", "28": "Smart Advisor Controller",
"29": "Testimonials Management Controller", "29": "Testimonials Management Controller",
"30": "src/services/api.ts", "30": "Admin Sidebar and Layout",
"31": "Spinner.tsx", "31": "Admin Settings Managers",
"32": "useCartStore", "32": "User Profile and Success Pages",
"33": "ZibalService", "33": "auth.service.ts",
"34": "main.ts", "34": "main.ts",
"35": "payment.controller.ts", "35": "ContactService",
"36": "Backend TypeScript Config", "36": "Backend TypeScript Config",
"37": "App TypeScript Config", "37": "App TypeScript Config",
"38": "ConfirmModal.tsx", "38": "CMS and Media Modals",
"39": "dependencies", "39": "dependencies",
"40": "Node TypeScript Config", "40": "Node TypeScript Config",
"41": "Admin Blog Controller", "41": "Admin Blog Controller",
"42": "WholesaleService", "42": "WholesaleController",
"43": "devDependencies", "43": "Frontend Testing and Styles",
"44": "Generic CRUD Controller", "44": "Generic CRUD Controller",
"45": "seo.module.ts", "45": "seo.module.ts",
"46": "Coupon Management UI", "46": "Coupon Management UI",
"47": "Project Build Scripts", "47": "Project Build Scripts",
"48": "Home Data Module", "48": "Home Data Module",
"49": "devDependencies", "49": "Linting and PostCSS Config",
"50": "Database Seeding Logic", "50": "Database Seeding Logic",
"51": "Prisma Database Migrations", "51": "Prisma Database Migrations",
"52": "dependencies", "52": "Frontend Core Dependencies",
"53": "UI Skeleton and Tables", "53": "UI Skeleton and Tables",
"54": "components/Skeleton.tsx", "54": "Shop Loading States",
"55": "BE-001", "55": "Product Detail Page",
"56": "VetGallery.tsx", "56": "Video Gallery Components",
"57": "NPM Lifecycle Scripts", "57": "NPM Lifecycle Scripts",
"58": "Pet Management API", "58": "Pet Management API",
"59": "PrismaService", "59": "PrismaService",
"60": "Jest Testing Config", "60": "Jest Testing Config",
"61": "PaginationDto", "61": "PaginationDto",
"62": "WikiController", "62": "BlogsController",
"63": "FE-001", "63": "WholesaleApplyDto",
"64": "PetsService", "64": "SmsService",
"65": "rules/graphify.md", "65": "rules/graphify.md",
"66": "App Health Controller", "66": "App Health Controller",
"67": "dependencies", "67": "Frontend UI Dependencies",
"68": "OrdersService", "68": "Pet Business Logic",
"69": "Integrity Validation Scripts", "69": "Integrity Validation Scripts",
"70": "Admin Panel Package Config", "70": "Admin Panel Package Config",
"71": "Analytics and Report Charts", "71": "Analytics and Report Charts",
"72": "Task Orchestration Scripts", "72": "Task Orchestration Scripts",
"73": "Backend Package Config", "73": "Backend Package Config",
"74": "CreateHealthLogDto", "74": "Health Log DTOs",
"75": "React Error Boundary", "75": "React Error Boundary",
"76": "Application Package Config", "76": "Application Package Config",
"77": ".findAll", "77": ".findAll",
@ -81,7 +81,7 @@
"79": "VPN Utility Scripts", "79": "VPN Utility Scripts",
"80": "NestJS CLI Config", "80": "NestJS CLI Config",
"81": "Seed TypeScript Config", "81": "Seed TypeScript Config",
"82": "ADM-001", "82": "Order Business Logic",
"83": "Browser Utility Scripts", "83": "Browser Utility Scripts",
"84": "Dev Server Startup", "84": "Dev Server Startup",
"85": "Architectural Audit Findings", "85": "Architectural Audit Findings",
@ -91,8 +91,8 @@
"89": "Evidence Validation Scripts", "89": "Evidence Validation Scripts",
"90": "Blog Listing Page", "90": "Blog Listing Page",
"91": "Blog Post Detail Page", "91": "Blog Post Detail Page",
"92": "@types/node", "92": "Node Type Definitions",
"93": "typescript", "93": "TypeScript Language",
"94": "UI Text Seeding", "94": "UI Text Seeding",
"95": "Wiki Terms Seeding", "95": "Wiki Terms Seeding",
"96": "Blog Management DTOs", "96": "Blog Management DTOs",
@ -101,8 +101,8 @@
"99": "Wiki Page Routing", "99": "Wiki Page Routing",
"100": "Network Status Banner", "100": "Network Status Banner",
"101": "Docker Deployment Scripts", "101": "Docker Deployment Scripts",
"102": "DB-001", "102": "ESLint Core Configuration",
"103": "CreateReminderDto", "103": "TypeScript ESLint Support",
"104": "Database Migration Scripts", "104": "Database Migration Scripts",
"105": "Scientific Terms Schema", "105": "Scientific Terms Schema",
"106": "Blog Data Seeding", "106": "Blog Data Seeding",
@ -116,29 +116,29 @@
"114": "Manifest Data Generation", "114": "Manifest Data Generation",
"115": "Honest Manifest Synchronization", "115": "Honest Manifest Synchronization",
"116": "Manifest Entry Synchronization", "116": "Manifest Entry Synchronization",
"117": "Videos.tsx", "117": "Pet Reminder DTOs",
"118": "TS-001", "118": "React DOM Library",
"119": "TEST-001", "119": "Zustand State Management",
"120": "@tailwindcss/postcss", "120": "Tailwind PostCSS Plugin",
"121": "@types/react-dom", "121": "React DOM Type Definitions",
"122": "Admin Panel TSConfig", "122": "Admin Panel TSConfig",
"123": "About Page Component", "123": "About Page Component",
"124": "Privacy Page Component", "124": "Privacy Page Component",
"125": "Next.js Security Configuration", "125": "Next.js Security Configuration",
"126": "Typography and Font Assets", "126": "Typography and Font Assets",
"127": "DEVOPS-001", "127": "Bcrypt Password Hashing",
"128": "DOC-001", "128": "Helmet Security Middleware",
"129": "What You Must Do When Invoked", "129": "What You Must Do When Invoked",
"130": "RolesGuard", "130": "NestJS Core Framework",
"131": "راهنمای تست سیستم (Software Testing)", "131": "NestJS JWT Authentication",
"132": "Role & Core Objective", "132": "NestJS Swagger Documentation",
"133": "Required Review Group Closures", "133": "NestJS Rate Limiting",
"134": "Operational Rules & Boundaries", "134": "Passport JWT Strategy",
"135": "Operational Rules & Boundaries", "135": "Prisma Client Library",
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3", "136": "Swagger UI Express",
"137": "@nestjs/cli", "137": "NestJS CLI Tooling",
"138": "Operational Rules & Boundaries", "138": "NestJS Testing Utilities",
"139": "Operational Rules & Boundaries", "139": "TypeScript Jest Integration",
"140": "Bcrypt Type Definitions", "140": "Bcrypt Type Definitions",
"141": "Passport JWT Type Definitions", "141": "Passport JWT Type Definitions",
"142": "Supertest Type Definitions", "142": "Supertest Type Definitions",
@ -176,7 +176,7 @@
"174": "Production Docker Setup", "174": "Production Docker Setup",
"175": "Staging Docker Setup", "175": "Staging Docker Setup",
"176": ".agents/workflows/graphify.md", "176": ".agents/workflows/graphify.md",
"177": "Role & Core Objective", "177": "BlogsService",
"178": "graphify reference: extra exports and benchmark", "178": "graphify reference: extra exports and benchmark",
"179": "graphify reference: query, path, explain", "179": "graphify reference: query, path, explain",
"180": "graphify reference: add a URL and watch a folder", "180": "graphify reference: add a URL and watch a folder",
@ -184,87 +184,12 @@
"182": "graphify reference: incremental update and cluster-only", "182": "graphify reference: incremental update and cluster-only",
"183": "graphify reference: GitHub clone and cross-repo merge", "183": "graphify reference: GitHub clone and cross-repo merge",
"184": "graphify reference: transcribe video and audio", "184": "graphify reference: transcribe video and audio",
"185": "Reconciled Audit Roles & Assignments", "185": "AGENTS.md",
"186": "instructions.md", "186": "instructions.md",
"187": "Phase 3.1 — Human Review Preparation and Master Backlog Critique", "187": "js-yaml",
"188": "CLAUDE.md", "188": "CLAUDE.md",
"189": ".claude/CLAUDE.md", "189": ".claude/CLAUDE.md",
"190": "extraction-spec.md", "190": "extraction-spec.md",
"191": "User Login API", "191": "User Login API",
"192": "Developer Standards and Architecture", "192": "Developer Standards and Architecture"
"193": "Deep Audit Summary Report",
"194": "Operational Rules & Boundaries",
"195": "Comprehensive Change Log",
"196": "Operational Rules & Boundaries",
"197": "1. Summary of Integrity Repairs Performed",
"198": "Operational Rules & Boundaries",
"199": "Operational Rules & Boundaries",
"200": "Operational Rules & Boundaries",
"201": "Vazirmatn Changelog",
"202": "Vazirmatn Font فونت وزیرمتن",
"203": "Operational Rules & Boundaries",
"204": "backend/README.md",
"205": "Repository Map",
"206": "Sahel-Font",
"207": "AuthService",
"208": "Sahel-Font",
"209": "Role & Core Objective",
"210": "exclude",
"211": "Phase 2 Final Quality Gate Summary Report",
"212": "Task Modifications Log",
"213": "Install",
"214": "BlogsController",
"215": "System Discovery",
"216": "Product Requirement Document (PRD)",
"217": "Baseline Command Plan & Reconciled Command History",
"218": "Architecture Specification",
"219": "Project Health Audit Report",
"220": "Open Questions",
"221": "Final Phase 2 Audit Closure Report",
"222": "📝 Active Agent Working Scratchpad",
"223": "🔍 Code Health Audit Review (01_auditor)",
"224": "Omitted File Inspection Report",
"225": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"226": "Phase 3 Audit Traceability Matrix",
"227": "API Contract Specification",
"228": "⚙️ Backend Technical Review (05_dev_backend)",
"229": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"230": "🚀 SEO & Content Strategy Review (12_seo_content)",
"231": "Raw Finding Verification & Disposition Report",
"232": "React + TypeScript + Vite",
"233": "application/README.md",
"234": "🔒 Security & Performance Review (09_devops_security)",
"235": "👁️ UX & Persona Interface Review (08_visual_qa)",
"236": "Compiler Diagnostic Dispositions",
"237": "@eslint/eslintrc",
"238": "eslint-plugin-prettier",
"239": "globals",
"240": "prettier",
"241": "prisma",
"242": "supertest",
"243": "ts-loader",
"244": "ts-node",
"245": "@types/express",
"246": "@types/jest",
"247": "@types/js-yaml",
"248": "@types/multer",
"249": "eslint-plugin-react-hooks",
"250": "eslint-plugin-react-refresh",
"251": "tailwindcss",
"252": "typescript",
"253": "@testing-library/jest-dom",
"254": "@testing-library/react",
"255": "@types/react",
"256": "typescript",
"257": "vitest",
"258": "reviews/README.md",
"259": "axios",
"260": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"261": "shabnam-font-v5.0.1/CHANGELOG.md",
"262": "application/CLAUDE.md",
"263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"264": "tailwindcss",
"265": "typescript-eslint",
"268": "wholesale.controller.ts",
"273": "AGENTS.md"
} }

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,26 +1,26 @@
# Graph Report - canina (2026-08-16) # Graph Report - canina (2026-08-16)
## Corpus Check ## Corpus Check
- 463 files · ~678,911 words - 459 files · ~667,716 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 3234 nodes · 5269 edges · 285 communities (172 shown, 113 thin omitted) - 3167 nodes · 5051 edges · 265 communities (171 shown, 94 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 186 edges (avg confidence: 0.79) - Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 160 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `8de13513` - Built from commit: `03900e72`
- 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)
- AdminService - AdminService
- pets/pets.controller.ts - Pet Management Controller
- Roles - SettingsController
- UsersService - UsersService
- CmsController - CMS Content Management
- BannersService - OrdersService
- auth.controller.ts - auth.controller.ts
- app.module.ts - app.module.ts
- CreateVideoDto - CreateVideoDto
@ -28,32 +28,32 @@
- SmartAdvisor.tsx - SmartAdvisor.tsx
- compilerOptions - compilerOptions
- toPersian - toPersian
- SmsService - Media and Blog UI Components
- ProductsService - ProductsService
- lib/services/api.ts - lib/services/api.ts
- eslint - devDependencies
- useSettingsStore - useSettingsStore
- AppModule - AppModule
- B2B Inquiry Controller - B2B Inquiry Controller
- Category Management Controller - Category Management Controller
- auth.service.ts - Banner Management Controller
- MediaController - Media Upload Controller
- Ingredient Management Controller - Ingredient Management Controller
- adminRoutes.tsx - Admin Dashboard Components
- admin.module.ts - admin.module.ts
- ConfirmModal.tsx - Pet DTOs and Controller
- Prescription Review Controller - Prescription Review Controller
- Smart Advisor Controller - Smart Advisor Controller
- Testimonials Management Controller - Testimonials Management Controller
- src/services/api.ts - Admin Sidebar and Layout
- Spinner.tsx - Admin Settings Managers
- useCartStore - useCartStore
- ZibalService - ZibalService
- main.ts - main.ts
- ContactService - ContactService
- Backend TypeScript Config - Backend TypeScript Config
- App TypeScript Config - App TypeScript Config
- Coupons.tsx - CMS and Media Modals
- dependencies - dependencies
- Node TypeScript Config - Node TypeScript Config
- Admin Blog Controller - Admin Blog Controller
@ -61,7 +61,7 @@
- devDependencies - devDependencies
- Generic CRUD Controller - Generic CRUD Controller
- seo.module.ts - seo.module.ts
- OrdersController - Coupon Management UI
- Project Build Scripts - Project Build Scripts
- Home Data Module - Home Data Module
- devDependencies - devDependencies
@ -77,19 +77,19 @@
- PrismaService - PrismaService
- Jest Testing Config - Jest Testing Config
- PaginationDto - PaginationDto
- SettingsService - BlogsController
- FE-001 - FE-001
- payment.module.ts - pagination.dto.ts
- rules/graphify.md - rules/graphify.md
- App Health Controller - App Health Controller
- dependencies - dependencies
- OrdersService - Pet Business Logic
- Integrity Validation Scripts - Integrity Validation Scripts
- Admin Panel Package Config - Admin Panel Package Config
- Analytics and Report Charts - Analytics and Report Charts
- Task Orchestration Scripts - Task Orchestration Scripts
- Backend Package Config - Backend Package Config
- BlogsController - Health Log DTOs
- React Error Boundary - React Error Boundary
- Application Package Config - Application Package Config
- WikiController - WikiController
@ -108,7 +108,7 @@
- Blog Listing Page - Blog Listing Page
- Blog Post Detail Page - Blog Post Detail Page
- @types/node - @types/node
- devDependencies - typescript
- UI Text Seeding - UI Text Seeding
- Wiki Terms Seeding - Wiki Terms Seeding
- Blog Management DTOs - Blog Management DTOs
@ -118,7 +118,7 @@
- Network Status Banner - Network Status Banner
- Docker Deployment Scripts - Docker Deployment Scripts
- DB-001 - DB-001
- BlogsService - typescript-eslint
- Database Migration Scripts - Database Migration Scripts
- Scientific Terms Schema - Scientific Terms Schema
- Blog Data Seeding - Blog Data Seeding
@ -132,10 +132,10 @@
- Manifest Data Generation - Manifest Data Generation
- Honest Manifest Synchronization - Honest Manifest Synchronization
- Manifest Entry Synchronization - Manifest Entry Synchronization
- WikiService - Pet Reminder DTOs
- TS-001 - TS-001
- TEST-001 - TEST-001
- ValidateCouponDto - @tailwindcss/postcss
- @types/react-dom - @types/react-dom
- Admin Panel TSConfig - Admin Panel TSConfig
- About Page Component - About Page Component
@ -145,19 +145,19 @@
- DEVOPS-001 - DEVOPS-001
- DOC-001 - DOC-001
- What You Must Do When Invoked - What You Must Do When Invoked
- JwtAuthGuard - Roles
- راهنمای تست سیستم (Software Testing) - راهنمای تست سیستم (Software Testing)
- Role & Core Objective - Role & Core Objective
- Required Review Group Closures - Required Review Group Closures
- Operational Rules & Boundaries - Operational Rules & Boundaries
- Operational Rules & Boundaries - Operational Rules & Boundaries
- 🏢 AI Software Agency — Master Orchestration Protocol v3 - 🏢 AI Software Agency — Master Orchestration Protocol v3
- @nestjs/cli - NestJS CLI Tooling
- Operational Rules & Boundaries - Operational Rules & Boundaries
- Operational Rules & Boundaries - Operational Rules & Boundaries
- Bcrypt Type Definitions - Bcrypt Type Definitions
- bcryptjs - Passport JWT Type Definitions
- helmet - Supertest Type Definitions
- Blog Entity Model - Blog Entity Model
- Home Entity Model - Home Entity Model
- Wiki Entity Model - Wiki Entity Model
@ -246,7 +246,7 @@
- 👁️ UX & Persona Interface Review (08_visual_qa) - 👁️ UX & Persona Interface Review (08_visual_qa)
- Compiler Diagnostic Dispositions - Compiler Diagnostic Dispositions
- @eslint/eslintrc - @eslint/eslintrc
- js-yaml - eslint-plugin-prettier
- globals - globals
- prettier - prettier
- prisma - prisma
@ -266,38 +266,18 @@
- @types/react - @types/react
- typescript - typescript
- vitest - vitest
- typescript-eslint
- @nestjs/core
- @nestjs/jwt
- @nestjs/swagger
- @nestjs/throttler
- passport-jwt
- @prisma/client
- swagger-ui-express
- AGENTS.md
- eslint-config-prettier
- @eslint/js
- jest
- @nestjs/schematics
- @nestjs/testing
- source-map-support
- ts-jest
- tsconfig-paths
- @types/bcryptjs
- typescript-eslint
- globals
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `PrismaService` - 74 edges 1. `PrismaService` - 72 edges
2. `Roles()` - 59 edges 2. `Roles()` - 45 edges
3. `PaginationDto` - 39 edges 3. `PaginationDto` - 39 edges
4. `SmsService` - 38 edges 4. `useSettingsStore` - 33 edges
5. `api` - 34 edges 5. `api` - 32 edges
6. `useSettingsStore` - 33 edges 6. `AdminService` - 31 edges
7. `AdminService` - 31 edges 7. `toPersian()` - 31 edges
8. `toPersian()` - 31 edges 8. `AdminController` - 30 edges
9. `AdminController` - 30 edges 9. `useCartStore` - 29 edges
10. `useCartStore` - 29 edges 10. `JwtAuthGuard` - 27 edges
## Surprising Connections (you probably didn't know these) ## Surprising Connections (you probably didn't know these)
- `User Profile Photo` --conceptually_related_to--> `User Roles and Capabilities` [INFERRED] - `User Profile Photo` --conceptually_related_to--> `User Roles and Capabilities` [INFERRED]
@ -312,47 +292,47 @@
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/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` - 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`
- 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`
## Hyperedges (group relationships) ## Hyperedges (group relationships)
- **Rastikerdar Font Ecosystem** — frontend_application_public_fonts_shabnam_font_v5_0_1_readme, frontend_application_public_fonts_vazirmatn_v33_003_readme, vazir_font [EXTRACTED 0.95] - **Rastikerdar Font Ecosystem** — frontend_application_public_fonts_shabnam_font_v5_0_1_readme, frontend_application_public_fonts_vazirmatn_v33_003_readme, vazir_font [EXTRACTED 0.95]
- **Canina Deployment Stack** — scripts_compose_prod, scripts_compose_stage [EXTRACTED 1.00] - **Canina Deployment Stack** — scripts_compose_prod, scripts_compose_stage [EXTRACTED 1.00]
## Communities (285 total, 113 thin omitted) ## Communities (265 total, 94 thin omitted)
### Community 0 - "AdminService" ### Community 0 - "AdminService"
Cohesion: 0.07 Cohesion: 0.07
Nodes (22): AdminController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+14 more) Nodes (22): AdminController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+14 more)
### Community 1 - "pets/pets.controller.ts" ### Community 1 - "Pet Management Controller"
Cohesion: 0.05 Cohesion: 0.17
Nodes (45): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+37 more) Nodes (18): PetsController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+10 more)
### Community 2 - "Roles" ### Community 2 - "SettingsController"
Cohesion: 0.19 Cohesion: 0.13
Nodes (16): Roles(), SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+8 more) Nodes (16): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+8 more)
### Community 3 - "UsersService" ### Community 3 - "UsersService"
Cohesion: 0.07 Cohesion: 0.06
Nodes (33): JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty (+25 more) Nodes (37): AuthModule, Module, JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional (+29 more)
### Community 4 - "CmsController" ### Community 4 - "CMS Content Management"
Cohesion: 0.09 Cohesion: 0.09
Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more) Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
### Community 5 - "BannersService" ### Community 5 - "OrdersService"
Cohesion: 0.13 Cohesion: 0.07
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more) Nodes (29): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+21 more)
### Community 6 - "auth.controller.ts" ### Community 6 - "auth.controller.ts"
Cohesion: 0.07 Cohesion: 0.05
Nodes (39): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+31 more) Nodes (41): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+33 more)
### Community 7 - "app.module.ts" ### Community 7 - "app.module.ts"
Cohesion: 0.10 Cohesion: 0.09
Nodes (25): AuthModule, Module, B2BModule, Module, BannersModule, Module, CmsModule, Module (+17 more) Nodes (28): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+20 more)
### Community 8 - "CreateVideoDto" ### Community 8 - "CreateVideoDto"
Cohesion: 0.07 Cohesion: 0.07
@ -374,6 +354,10 @@ Nodes (32): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModu
Cohesion: 0.08 Cohesion: 0.08
Nodes (37): ClientLayout(), VerifyContent(), AddressModal(), AddressModalProps, AuthModal(), AuthModalProps, B2BPortal(), BackButton() (+29 more) Nodes (37): ClientLayout(), VerifyContent(), AddressModal(), AddressModalProps, AuthModal(), AuthModalProps, B2BPortal(), BackButton() (+29 more)
### Community 13 - "Media and Blog UI Components"
Cohesion: 0.09
Nodes (19): Media, MediaSelector(), MediaSelectorProps, Pagination(), PaginationProps, BlogPost, Category, Pet (+11 more)
### Community 14 - "ProductsService" ### Community 14 - "ProductsService"
Cohesion: 0.09 Cohesion: 0.09
Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more) Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
@ -382,10 +366,18 @@ Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, P
Cohesion: 0.08 Cohesion: 0.08
Nodes (17): metadata, ContactFormClient(), ContactInfoItem, PrescriptionUploadModal(), PrescriptionUploadModalProps, api, ApiErrorPayload, ApiErr (+9 more) Nodes (17): metadata, ContactFormClient(), ContactInfoItem, PrescriptionUploadModal(), PrescriptionUploadModalProps, api, ApiErrorPayload, ApiErr (+9 more)
### Community 16 - "devDependencies"
Cohesion: 0.09
Nodes (23): devDependencies, eslint, eslint-config-prettier, @eslint/js, jest, @nestjs/schematics, @nestjs/testing, source-map-support (+15 more)
### Community 17 - "useSettingsStore" ### Community 17 - "useSettingsStore"
Cohesion: 0.07 Cohesion: 0.07
Nodes (30): HomeClient(), HomeClientProps, getHomeData(), Home(), metadata, metadata, EnamadBadge(), Footer() (+22 more) Nodes (30): HomeClient(), HomeClientProps, getHomeData(), Home(), metadata, metadata, EnamadBadge(), Footer() (+22 more)
### Community 18 - "AppModule"
Cohesion: 0.12
Nodes (7): ApiExcludeController, AppModule, Module, MetricsController, Controller, Get, Res
### Community 19 - "B2B Inquiry Controller" ### Community 19 - "B2B Inquiry Controller"
Cohesion: 0.13 Cohesion: 0.13
Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+7 more) Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+7 more)
@ -394,29 +386,29 @@ Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controlle
Cohesion: 0.10 Cohesion: 0.10
Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more) Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more)
### Community 21 - "auth.service.ts" ### Community 21 - "Banner Management Controller"
Cohesion: 0.10 Cohesion: 0.13
Nodes (10): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, RedisModule, Global, Module (+2 more) Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
### Community 22 - "MediaController" ### Community 22 - "Media Upload Controller"
Cohesion: 0.10 Cohesion: 0.11
Nodes (16): MediaController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more) Nodes (16): MediaController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
### Community 23 - "Ingredient Management Controller" ### Community 23 - "Ingredient Management Controller"
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 24 - "adminRoutes.tsx" ### Community 24 - "Admin Dashboard Components"
Cohesion: 0.08 Cohesion: 0.10
Nodes (18): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, PatternItem, SmsConfigState, SmsLogItem (+10 more) Nodes (14): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, GROUP_PAGE_MAP, GROUPS, PAGE_TABS (+6 more)
### Community 25 - "admin.module.ts" ### Community 25 - "admin.module.ts"
Cohesion: 0.10 Cohesion: 0.07
Nodes (14): AdminModule, Module, PetQuery, PetsService, Injectable, ReportsController, ApiBearerAuth, ApiOperation (+6 more) Nodes (20): AdminModule, Module, BlogQuery, BlogsService, Injectable, PetQuery, PetsService, Injectable (+12 more)
### Community 26 - "ConfirmModal.tsx" ### Community 26 - "Pet DTOs and Controller"
Cohesion: 0.10 Cohesion: 0.16
Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Pet, fetchVideosList(), Video, Videos() (+8 more) Nodes (13): CreatePetDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional, IsString (+5 more)
### Community 27 - "Prescription Review Controller" ### Community 27 - "Prescription Review Controller"
Cohesion: 0.14 Cohesion: 0.14
@ -430,21 +422,21 @@ Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body,
Cohesion: 0.13 Cohesion: 0.13
Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more) Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 30 - "src/services/api.ts" ### Community 30 - "Admin Sidebar and Layout"
Cohesion: 0.10 Cohesion: 0.17
Nodes (27): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+19 more) Nodes (16): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+8 more)
### Community 31 - "Spinner.tsx" ### Community 31 - "Admin Settings Managers"
Cohesion: 0.08 Cohesion: 0.09
Nodes (23): Media, MediaSelector(), MediaSelectorProps, Spinner(), BlogPost, Category, Category, Product (+15 more) Nodes (21): Spinner(), B2BManager, BannersManager, FinancialSettingsPage, SeoSettingsPage, SmartAdvisorManager, SystemSettingsPage, TestimonialsManager (+13 more)
### Community 32 - "useCartStore" ### Community 32 - "useCartStore"
Cohesion: 0.12 Cohesion: 0.12
Nodes (16): metadata, ArchiveProductCard(), ProductCard(), Header(), MENU_ICONS, OrderSuccess(), OrderTracking(), PetProfile() (+8 more) Nodes (16): metadata, ArchiveProductCard(), ProductCard(), Header(), MENU_ICONS, OrderSuccess(), OrderTracking(), PetProfile() (+8 more)
### Community 33 - "ZibalService" ### Community 33 - "ZibalService"
Cohesion: 0.06 Cohesion: 0.07
Nodes (37): AdminTransactionFilterDto, ApiPropertyOptional, IsNumber, IsOptional, IsString, Type, InitiatePaymentDto, InitiateWalletTopupDto (+29 more) Nodes (30): InitiatePaymentDto, InitiateWalletTopupDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+22 more)
### Community 34 - "main.ts" ### Community 34 - "main.ts"
Cohesion: 0.14 Cohesion: 0.14
@ -462,13 +454,13 @@ Nodes (22): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration,
Cohesion: 0.09 Cohesion: 0.09
Nodes (22): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+14 more) Nodes (22): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+14 more)
### Community 38 - "Coupons.tsx" ### Community 38 - "CMS and Media Modals"
Cohesion: 0.09 Cohesion: 0.10
Nodes (15): Pagination(), PaginationProps, Coupon, CouponFormData, CouponModalProps, CouponTarget, Media, Stats (+7 more) Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Media, fetchVideosList(), Video, Videos() (+8 more)
### Community 39 - "dependencies" ### Community 39 - "dependencies"
Cohesion: 0.10 Cohesion: 0.05
Nodes (21): dependencies, bcrypt, class-transformer, class-validator, ioredis, @nestjs/common, @nestjs/passport, @nestjs/platform-express (+13 more) Nodes (41): dependencies, bcrypt, bcryptjs, class-transformer, class-validator, helmet, ioredis, js-yaml (+33 more)
### Community 40 - "Node TypeScript Config" ### Community 40 - "Node TypeScript Config"
Cohesion: 0.10 Cohesion: 0.10
@ -494,9 +486,9 @@ Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, De
Cohesion: 0.16 Cohesion: 0.16
Nodes (10): SeoController, ApiOperation, ApiTags, Controller, Get, Param, SeoModule, Module (+2 more) Nodes (10): SeoController, ApiOperation, ApiTags, Controller, Get, Param, SeoModule, Module (+2 more)
### Community 46 - "OrdersController" ### Community 46 - "Coupon Management UI"
Cohesion: 0.13 Cohesion: 0.25
Nodes (16): OrdersController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+8 more) Nodes (5): Coupon, CouponFormData, CouponModalProps, CouponTarget, Coupons
### Community 47 - "Project Build Scripts" ### Community 47 - "Project Build Scripts"
Cohesion: 0.11 Cohesion: 0.11
@ -508,7 +500,7 @@ Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Ge
### Community 49 - "devDependencies" ### Community 49 - "devDependencies"
Cohesion: 0.11 Cohesion: 0.11
Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, postcss, @tailwindcss/postcss, @types/node (+11 more) Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, globals, postcss, @types/node (+11 more)
### Community 50 - "Database Seeding Logic" ### Community 50 - "Database Seeding Logic"
Cohesion: 0.17 Cohesion: 0.17
@ -547,8 +539,8 @@ Cohesion: 0.15
Nodes (11): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+3 more) Nodes (11): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+3 more)
### Community 59 - "PrismaService" ### Community 59 - "PrismaService"
Cohesion: 0.08 Cohesion: 0.06
Nodes (18): ApiExcludeController, CouponTargetInput, PaginationQuery, CategoryQuery, MetricsController, Controller, Get, Res (+10 more) Nodes (21): CouponTargetInput, PaginationQuery, CategoryQuery, AdminLoginInput, LoginInput, RegisterInput, MeliPayamakResponse, SendPatternSmsOptions (+13 more)
### Community 60 - "Jest Testing Config" ### Community 60 - "Jest Testing Config"
Cohesion: 0.15 Cohesion: 0.15
@ -556,19 +548,19 @@ Nodes (13): jest, collectCoverageFrom, coverageDirectory, moduleFileExtensions,
### Community 61 - "PaginationDto" ### Community 61 - "PaginationDto"
Cohesion: 0.13 Cohesion: 0.13
Nodes (13): PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsOptional, IsString, Type, Module (+5 more) Nodes (12): PaginationDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, Type, Module, WikiModule (+4 more)
### Community 62 - "SettingsService" ### Community 62 - "BlogsController"
Cohesion: 0.11 Cohesion: 0.20
Nodes (4): SmsLogQuery, ScientificTermData, SettingsService, Injectable Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable
### Community 63 - "FE-001" ### Community 63 - "FE-001"
Cohesion: 0.06 Cohesion: 0.06
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Architecture Overview & Confirmed Strengths, Category, Completion Statement, Confidence (+23 more) Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Architecture Overview & Confirmed Strengths, Category, Completion Statement, Confidence (+23 more)
### Community 64 - "payment.module.ts" ### Community 64 - "pagination.dto.ts"
Cohesion: 0.16 Cohesion: 0.11
Nodes (10): SmsModule, Global, Module, ContactModule, Module, PaymentModule, Module, ZibalInquiryResponse (+2 more) Nodes (10): Injectable, WikiQuery, WikiService, SortOrder, IsNotEmpty, IsNumber, IsString, ValidateCouponDto (+2 more)
### Community 66 - "App Health Controller" ### Community 66 - "App Health Controller"
Cohesion: 0.29 Cohesion: 0.29
@ -578,10 +570,6 @@ Nodes (5): AppController, Controller, Get, AppService, Injectable
Cohesion: 0.12 Cohesion: 0.12
Nodes (17): dependencies, axios, lucide-react, motion, next, react, react-dom, sonner (+9 more) Nodes (17): dependencies, axios, lucide-react, motion, next, react, react-dom, sonner (+9 more)
### Community 68 - "OrdersService"
Cohesion: 0.12
Nodes (15): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+7 more)
### Community 69 - "Integrity Validation Scripts" ### Community 69 - "Integrity Validation Scripts"
Cohesion: 0.20 Cohesion: 0.20
Nodes (9): dispSum, errors, invalidNewIds, manifest, report, trackedFiles, uninspected, verifiedIndex (+1 more) Nodes (9): dispSum, errors, invalidNewIds, manifest, report, trackedFiles, uninspected, verifiedIndex (+1 more)
@ -602,9 +590,9 @@ Nodes (8): cmd_next_task(), cmd_progress(), cmd_reset(), cmd_status(), cmd_unblo
Cohesion: 0.22 Cohesion: 0.22
Nodes (8): author, description, license, name, prisma, seed, private, version Nodes (8): author, description, license, name, prisma, seed, private, version
### Community 74 - "BlogsController" ### Community 74 - "Health Log DTOs"
Cohesion: 0.20 Cohesion: 0.29
Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable Nodes (6): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
### Community 75 - "React Error Boundary" ### Community 75 - "React Error Boundary"
Cohesion: 0.22 Cohesion: 0.22
@ -666,10 +654,6 @@ Nodes (4): Blog(), getBlogs(), metadata, BlogPage()
Cohesion: 0.60 Cohesion: 0.60
Nodes (4): BlogPostPage(), generateMetadata(), getBlog(), revalidate Nodes (4): BlogPostPage(), generateMetadata(), getBlog(), revalidate
### Community 93 - "devDependencies"
Cohesion: 0.22
Nodes (9): devDependencies, eslint-plugin-prettier, @types/passport-jwt, @types/supertest, typescript, typescript, eslint-plugin-prettier, @types/passport-jwt (+1 more)
### Community 99 - "Wiki Page Routing" ### Community 99 - "Wiki Page Routing"
Cohesion: 0.83 Cohesion: 0.83
Nodes (3): generateMetadata(), getWikiTerm(), WikiTermPage() Nodes (3): generateMetadata(), getWikiTerm(), WikiTermPage()
@ -682,17 +666,13 @@ Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script
Cohesion: 0.06 Cohesion: 0.06
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Database and Data Integrity Audit Report (+23 more) Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Database and Data Integrity Audit Report (+23 more)
### Community 103 - "BlogsService"
Cohesion: 0.22
Nodes (3): BlogQuery, BlogsService, Injectable
### Community 109 - "Auth Architecture and Planning" ### Community 109 - "Auth Architecture and Planning"
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
### Community 117 - "WikiService" ### Community 117 - "Pet Reminder DTOs"
Cohesion: 0.22 Cohesion: 0.29
Nodes (3): Injectable, WikiQuery, WikiService Nodes (6): CreateReminderDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
### Community 118 - "TS-001" ### Community 118 - "TS-001"
Cohesion: 0.06 Cohesion: 0.06
@ -702,10 +682,6 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
Cohesion: 0.06 Cohesion: 0.06
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Dependencies (+23 more) Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Dependencies (+23 more)
### Community 120 - "ValidateCouponDto"
Cohesion: 0.50
Nodes (4): IsNotEmpty, IsNumber, IsString, ValidateCouponDto
### Community 126 - "Typography and Font Assets" ### Community 126 - "Typography and Font Assets"
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
@ -722,9 +698,9 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
Cohesion: 0.07 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 130 - "JwtAuthGuard" ### Community 130 - "Roles"
Cohesion: 0.18 Cohesion: 0.21
Nodes (7): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload Nodes (8): JwtAuthGuard, Injectable, Roles(), ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
### Community 131 - "راهنمای تست سیستم (Software Testing)" ### Community 131 - "راهنمای تست سیستم (Software Testing)"
Cohesion: 0.07 Cohesion: 0.07
@ -955,24 +931,24 @@ Cohesion: 0.50
Nodes (3): Deploy on Vercel, Getting Started, Learn More Nodes (3): Deploy on Vercel, Getting Started, Learn More
## Knowledge Gaps ## Knowledge Gaps
- **1179 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1174 more) - **1170 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1165 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **113 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **94 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 `ApiResponse` connect `src/services/api.ts` to `pets/pets.controller.ts`, `UsersService`, `auth.controller.ts`, `BlogsController`, `WikiController`, `OrdersController`, `ProductsService`, `Home Data Module`?** - **Why does `ApiResponse` connect `Admin Sidebar and Layout` to `Pet Management Controller`, `UsersService`, `OrdersService`, `auth.controller.ts`, `WikiController`, `ProductsService`, `Home Data Module`, `BlogsController`, `Admin Settings Managers`?**
_High betweenness centrality (0.039) - this node is a cross-community bridge._ _High betweenness centrality (0.033) - this node is a cross-community bridge._
- **Why does `Roles()` connect `Roles` to `ZibalService`, `JwtAuthGuard`, `ContactService`, `CmsController`, `BannersService`, `WholesaleApplyDto`, `ProductsService`, `B2B Inquiry Controller`, `Ingredient Management Controller`, `Prescription Review Controller`, `Smart Advisor Controller`, `Testimonials Management Controller`, `SettingsService`?** - **Why does `Roles()` connect `Roles` to `SettingsController`, `ContactService`, `CMS Content Management`, `WholesaleApplyDto`, `ProductsService`, `B2B Inquiry Controller`, `Banner Management Controller`, `Ingredient Management Controller`, `Prescription Review Controller`, `Smart Advisor Controller`, `Testimonials Management Controller`?**
_High betweenness centrality (0.037) - this node is a cross-community bridge._ _High betweenness centrality (0.020) - this node is a cross-community bridge._
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `AdminService`, `pets/pets.controller.ts`, `UsersService`, `CmsController`, `OrdersService`, `CreateVideoDto`, `MediaController`, `admin.module.ts`, `SettingsService`?** - **Why does `JwtAuthGuard` connect `Roles` to `AdminService`, `pagination.dto.ts`, `ZibalService`, `UsersService`, `CreateVideoDto`, `ProductsService`, `admin.module.ts`, `Pet DTOs and Controller`?**
_High betweenness centrality (0.022) - this node is a cross-community bridge._ _High betweenness centrality (0.020) - 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?**
_1179 weakly-connected nodes found - possible documentation gaps or missing edges._ _1170 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `AdminService` be split into smaller, more focused modules?** - **Should `AdminService` be split into smaller, more focused modules?**
_Cohesion score 0.06841046277665996 - nodes in this community are weakly interconnected._ _Cohesion score 0.06690140845070422 - nodes in this community are weakly interconnected._
- **Should `pets/pets.controller.ts` be split into smaller, more focused modules?** - **Should `SettingsController` be split into smaller, more focused modules?**
_Cohesion score 0.053923541247484906 - nodes in this community are weakly interconnected._ _Cohesion score 0.1251778093883357 - nodes in this community are weakly interconnected._
- **Should `UsersService` be split into smaller, more focused modules?** - **Should `UsersService` be split into smaller, more focused modules?**
_Cohesion score 0.06766917293233082 - nodes in this community are weakly interconnected._ _Cohesion score 0.06093189964157706 - nodes in this community are weakly interconnected._

View File

@ -1 +0,0 @@
{"nodes": [{"id": "$graphify-root$_agents_md", "label": "AGENTS.md", "file_type": "document", "source_file": "AGENTS.md", "source_location": "L1"}, {"id": "$graphify-root$_agents_graphify", "label": "graphify", "file_type": "document", "source_file": "AGENTS.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_agents_md", "target": "$graphify-root$_agents_graphify", "relation": "contains", "confidence": "EXTRACTED", "source_file": "AGENTS.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

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