Compare commits
No commits in common. "5ace474d71f8132ffe015e56fda81fed9849076e" and "8992060cb74cb3647ecdbf06d6437ecd23d9313f" have entirely different histories.
5ace474d71
...
8992060cb7
@ -583,23 +583,3 @@ model Setting {
|
||||
@@index([category])
|
||||
@@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")
|
||||
}
|
||||
|
||||
|
||||
@ -29,8 +29,6 @@ import { PrescriptionsModule } from './prescriptions/prescriptions.module';
|
||||
import { B2BModule } from './b2b/b2b.module';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SmsModule,
|
||||
@ -79,18 +77,15 @@ export class AppModule implements NestModule {
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply((req: Request, res: Response, next: NextFunction) => {
|
||||
.apply((req: any, res: any, next: () => void) => {
|
||||
MetricsController.incrementRequestCount();
|
||||
|
||||
const url: string = req.originalUrl || req.url || '';
|
||||
const method: string = req.method || '';
|
||||
|
||||
// Exclude options, admin panel requests, static assets, and health metrics from visit counter
|
||||
const isAdminPath =
|
||||
url.includes('/admin') || url.includes('/api/admin');
|
||||
const isStaticAsset = url.match(
|
||||
/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i,
|
||||
);
|
||||
const isAdminPath = url.includes('/admin') || url.includes('/api/admin');
|
||||
const isStaticAsset = url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i);
|
||||
const isIgnoredMethod = method === 'OPTIONS' || method === 'HEAD';
|
||||
|
||||
if (!isAdminPath && !isStaticAsset && !isIgnoredMethod) {
|
||||
|
||||
@ -49,8 +49,7 @@ export class AuthService {
|
||||
// Remove OTP from Redis if SMS failed to avoid phantom codes
|
||||
await this.redisService.del(`otp:${phoneNumber}`);
|
||||
throw new BadRequestException({
|
||||
message:
|
||||
'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
|
||||
message: 'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
|
||||
error: 'SMS_DELIVERY_FAILED',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,646 +1,40 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as https from 'https';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
export interface SendPatternSmsOptions {
|
||||
to: string;
|
||||
bodyId: number; // MeliPayamak Shared Pattern Body ID
|
||||
args: string[]; // Dynamic variables inside pattern
|
||||
type?: string; // OTP, ORDER_CONFIRMATION, SHIPPING_TRACKING, B2B_NOTIFICATION, PET_CARE_REMINDER, TEST, GENERIC
|
||||
}
|
||||
|
||||
export interface SmsConfig {
|
||||
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 {
|
||||
interface MeliPayamakResponse {
|
||||
Value?: number;
|
||||
RetStatus?: number;
|
||||
StrRetStatus?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly logger = new Logger(SmsService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 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(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
const body = (bodyMatch ? bodyMatch[1] : '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/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}`);
|
||||
}
|
||||
}
|
||||
private readonly username = process.env.MELIPAYAMAK_USERNAME || '9364100228';
|
||||
private readonly password = process.env.MELIPAYAMAK_PASSWORD || '';
|
||||
|
||||
/**
|
||||
* Send Pattern SMS using MeliPayamak Shared Service Line (Bypasses Blacklist)
|
||||
*/
|
||||
async sendPatternSms(options: SendPatternSmsOptions): Promise<boolean> {
|
||||
const config = await this.getSmsConfig();
|
||||
const msgType = options.type || 'GENERIC_PATTERN';
|
||||
|
||||
if (!config.enabled) {
|
||||
this.logger.warn(`[SMS Disabled] SMS dispatch skipped for ${options.to}`);
|
||||
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,
|
||||
});
|
||||
if (!this.username || !this.password) {
|
||||
this.logger.error(
|
||||
`[SMS MISCONFIGURED] MELIPAYAMAK_PASSWORD is not set! ` +
|
||||
`SMS to ${options.to} (Pattern: ${options.bodyId}) was NOT sent. ` +
|
||||
`Set MELIPAYAMAK_USERNAME and MELIPAYAMAK_PASSWORD environment variables.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const payload = JSON.stringify({
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
text: options.args.join(';'),
|
||||
to: options.to,
|
||||
bodyId: options.bodyId,
|
||||
@ -657,9 +51,8 @@ export class SmsService {
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||
const val = json.Value ?? 0;
|
||||
@ -667,63 +60,25 @@ export class SmsService {
|
||||
this.logger.log(
|
||||
`[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);
|
||||
} else {
|
||||
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
|
||||
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);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
status: 'FAILED',
|
||||
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
|
||||
});
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
})();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on('error', (err: Error) => {
|
||||
void (async () => {
|
||||
req.on('error', (err) => {
|
||||
this.logger.error(
|
||||
`[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);
|
||||
})();
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
@ -732,190 +87,73 @@ export class SmsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SMS Dispatch from Admin Panel
|
||||
*/
|
||||
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
|
||||
* Send OTP Verification Code (Pattern 508079)
|
||||
*/
|
||||
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({
|
||||
to: phone,
|
||||
bodyId: config.otpBodyId,
|
||||
bodyId,
|
||||
args: [otpCode],
|
||||
type: 'OTP',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Order Confirmation SMS
|
||||
* Send Order Confirmation SMS (Pattern 508081)
|
||||
*/
|
||||
async sendOrderConfirmation(
|
||||
phone: string,
|
||||
orderNumber: string,
|
||||
amount: string,
|
||||
): Promise<boolean> {
|
||||
const config = await this.getSmsConfig();
|
||||
const bodyId = parseInt(
|
||||
process.env.MELIPAYAMAK_ORDER_BODY_ID || '508081',
|
||||
10,
|
||||
);
|
||||
return this.sendPatternSms({
|
||||
to: phone,
|
||||
bodyId: config.orderBodyId,
|
||||
bodyId,
|
||||
args: [orderNumber, amount],
|
||||
type: 'ORDER_CONFIRMATION',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Shipping Status SMS with Tracking Code
|
||||
* Send Shipping Status SMS with Tracking Code (Pattern 508082)
|
||||
*/
|
||||
async sendShippingNotification(
|
||||
phone: string,
|
||||
orderNumber: string,
|
||||
trackingCode: string,
|
||||
): Promise<boolean> {
|
||||
const config = await this.getSmsConfig();
|
||||
const bodyId = parseInt(
|
||||
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '508082',
|
||||
10,
|
||||
);
|
||||
return this.sendPatternSms({
|
||||
to: phone,
|
||||
bodyId: config.shippingBodyId,
|
||||
bodyId,
|
||||
args: [orderNumber, trackingCode],
|
||||
type: 'SHIPPING_TRACKING',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send B2B Application Notification SMS
|
||||
* Send B2B Application Notification SMS (Pattern 508083)
|
||||
*/
|
||||
async sendB2bNotification(
|
||||
phone: string,
|
||||
applicantName: string,
|
||||
): Promise<boolean> {
|
||||
const config = await this.getSmsConfig();
|
||||
const bodyId = parseInt(
|
||||
process.env.MELIPAYAMAK_B2B_BODY_ID || '508083',
|
||||
10,
|
||||
);
|
||||
return this.sendPatternSms({
|
||||
to: phone,
|
||||
bodyId: config.b2bBodyId,
|
||||
bodyId,
|
||||
args: [applicantName],
|
||||
type: 'B2B_NOTIFICATION',
|
||||
});
|
||||
}
|
||||
|
||||
@ -927,13 +165,14 @@ export class SmsService {
|
||||
petName: string,
|
||||
reminderType: string,
|
||||
): Promise<boolean> {
|
||||
const config = await this.getSmsConfig();
|
||||
if (!config.petCareBodyId) return false;
|
||||
const bodyId = parseInt(
|
||||
process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0',
|
||||
10,
|
||||
);
|
||||
return this.sendPatternSms({
|
||||
to: phone,
|
||||
bodyId: config.petCareBodyId,
|
||||
bodyId,
|
||||
args: [petName, reminderType],
|
||||
type: 'PET_CARE_REMINDER',
|
||||
});
|
||||
}
|
||||
|
||||
@ -941,24 +180,7 @@ export class SmsService {
|
||||
* Generic text SMS dispatch
|
||||
*/
|
||||
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}`);
|
||||
await this.recordLog({
|
||||
receptor: phone,
|
||||
type: 'GENERIC_TEXT',
|
||||
messageText: message,
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
}
|
||||
@ -18,19 +18,17 @@ import {
|
||||
InitiateWalletTopupDto,
|
||||
} from './dto/initiate-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 { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiResponse,
|
||||
ApiOkResponse,
|
||||
ApiBadRequestResponse,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Payment - درگاه پرداخت اینترنتی زیبال و مدیریت تراکنشها')
|
||||
@ApiTags('Payment - درگاه پرداخت اینترنتی زیبال (Zibal IPG)')
|
||||
@Controller('payment')
|
||||
export class PaymentController {
|
||||
constructor(
|
||||
@ -54,9 +52,7 @@ export class PaymentController {
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiBadRequestResponse({
|
||||
description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش',
|
||||
})
|
||||
@ApiBadRequestResponse({ description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش' })
|
||||
async initiateOrderPayment(
|
||||
@Req() req: { user: { id: string } },
|
||||
@Body() body: InitiatePaymentDto,
|
||||
@ -74,6 +70,14 @@ export class PaymentController {
|
||||
@ApiOperation({ summary: 'شارژ آنلاین کیف پول از طریق درگاه زیبال' })
|
||||
@ApiOkResponse({
|
||||
description: 'لینک هدایت به درگاه پرداخت برای شارژ کیف پول',
|
||||
schema: {
|
||||
example: {
|
||||
success: true,
|
||||
trackId: '15966442233311',
|
||||
paymentUrl: 'https://gateway.zibal.ir/start/15966442233311',
|
||||
amount: 500000,
|
||||
},
|
||||
},
|
||||
})
|
||||
async initiateWalletTopup(
|
||||
@Req() req: { user: { id: string } },
|
||||
@ -87,9 +91,7 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
@Get('zibal/callback')
|
||||
@ApiOperation({
|
||||
summary: 'دریافت کالبک بازگشت از درگاه زیبال و تایید تراکنش',
|
||||
})
|
||||
@ApiOperation({ summary: 'دریافت کالبک بازگشت از درگاه زیبال و تایید تراکنش' })
|
||||
async handleZibalCallback(
|
||||
@Query() query: ZibalCallbackQueryDto,
|
||||
@Res() res: Response,
|
||||
@ -128,10 +130,9 @@ export class PaymentController {
|
||||
});
|
||||
|
||||
return res.redirect(`${frontendUrl}/payment/verify?${params.toString()}`);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'خطا در پردازش پرداخت';
|
||||
} catch (err: any) {
|
||||
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;
|
||||
|
||||
if (!trackId) {
|
||||
return res
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.json({ success: false, message: 'trackId is required' });
|
||||
return res.status(HttpStatus.BAD_REQUEST).json({ success: false, message: 'trackId is required' });
|
||||
}
|
||||
|
||||
const result = await this.paymentService.verifyAndProcess(
|
||||
@ -168,42 +167,4 @@ export class PaymentController {
|
||||
async getTransactionStatus(@Param('trackId') trackId: string) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { ZibalService } from './zibal.service';
|
||||
import { SmsService } from '../common/services/sms.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AdminTransactionFilterDto } from './dto/admin-transaction-filter.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
@ -37,20 +36,21 @@ export class PaymentService {
|
||||
throw new NotFoundException('سفارش مورد نظر یافت نشد');
|
||||
}
|
||||
|
||||
if (order.status !== 'processing' && order.status !== 'pending_payment') {
|
||||
if (['shipped', 'delivered'].includes(order.status)) {
|
||||
throw new BadRequestException('این سفارش قبلاً تکمیل و ارسال شده است');
|
||||
}
|
||||
}
|
||||
|
||||
const amountTomans = Number(order.totalAmount);
|
||||
const amountRials = Math.round(amountTomans * 10);
|
||||
|
||||
if (amountRials < 1000) {
|
||||
throw new BadRequestException(
|
||||
'مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است',
|
||||
);
|
||||
throw new BadRequestException('مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است');
|
||||
}
|
||||
|
||||
const frontendUrl = await this.zibalService.getFrontendUrl();
|
||||
// Default callback points to frontend verify page (or backend proxy)
|
||||
const callbackUrl =
|
||||
customCallbackUrl || `${frontendUrl}/payment/verify?orderId=${order.id}`;
|
||||
|
||||
@ -111,11 +111,9 @@ export class PaymentService {
|
||||
orderId: order.id,
|
||||
amount: amountTomans,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.logger.error(`Error requesting payment from Zibal: ${msg}`, stack);
|
||||
throw new BadRequestException(msg || 'خطا در اتصال به درگاه پرداخت');
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error requesting payment from Zibal: ${err.message}`, err.stack);
|
||||
throw new BadRequestException(err.message || 'خطا در اتصال به درگاه پرداخت');
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,15 +189,14 @@ export class PaymentService {
|
||||
paymentUrl: this.zibalService.getStartUrl(trackId),
|
||||
amount: amountTomans,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
this.logger.error(`Error requesting wallet topup from Zibal: ${msg}`);
|
||||
throw new BadRequestException(msg || 'خطا در ارتباط با درگاه بانکی');
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error requesting wallet topup from Zibal: ${err.message}`);
|
||||
throw new BadRequestException(err.message || 'خطا در ارتباط با درگاه بانکی');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. Verify & Process Callback from Zibal (With Idempotency & Amount Match Check)
|
||||
* 3. Verify & Process Callback from Zibal
|
||||
*/
|
||||
async verifyAndProcess(
|
||||
trackId: string,
|
||||
@ -215,42 +212,34 @@ export class PaymentService {
|
||||
where: { trackId },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
user: true,
|
||||
orderItems: { include: { product: true } },
|
||||
},
|
||||
include: { user: true, orderItems: { include: { product: 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({
|
||||
where: { orderId: orderIdParam },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
user: true,
|
||||
orderItems: { include: { product: true } },
|
||||
},
|
||||
include: { user: true, orderItems: { include: { product: true } } },
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!transaction) {
|
||||
throw new NotFoundException('تراکنش پرداخت در سیستم یافت نشد');
|
||||
}
|
||||
|
||||
// --- IDEMPOTENCY CHECK ---
|
||||
// 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 is already verified, return successful details immediately
|
||||
if (transaction.status === 'VERIFIED') {
|
||||
this.logger.log(
|
||||
`Idempotency Guard: Transaction ${transaction.id} (trackId=${trackId}) is already VERIFIED. Skipping duplicate execution.`,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
alreadyVerified: true,
|
||||
@ -260,16 +249,13 @@ export class PaymentService {
|
||||
cardNumber: transaction.cardNumber,
|
||||
amount: Number(transaction.amount),
|
||||
orderId: transaction.orderId,
|
||||
type: transaction.type,
|
||||
message: 'تراکنش قبلاً با موفقیت تایید و پردازش شده است',
|
||||
};
|
||||
}
|
||||
|
||||
// If user cancelled or Zibal signaled failure at the gate
|
||||
if (successParam === '0' || statusParam === '3') {
|
||||
const statusMessage = this.zibalService.getStatusMessage(
|
||||
statusParam || 3,
|
||||
);
|
||||
const statusMessage = this.zibalService.getStatusMessage(statusParam || 3);
|
||||
await this.prisma.paymentTransaction.update({
|
||||
where: { id: transaction.id },
|
||||
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!
|
||||
const refNumber = verifyRes.refNumber
|
||||
? String(verifyRes.refNumber)
|
||||
: undefined;
|
||||
const refNumber = verifyRes.refNumber ? String(verifyRes.refNumber) : undefined;
|
||||
const cardNumber = verifyRes.cardNumber || undefined;
|
||||
const paidDate = verifyRes.paidAt
|
||||
? new Date(verifyRes.paidAt)
|
||||
: new Date();
|
||||
const paidDate = verifyRes.paidAt ? new Date(verifyRes.paidAt) : new Date();
|
||||
|
||||
// Execute atomic status update with concurrency guard
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// Optimistic lock: only update if still PENDING (prevents race condition)
|
||||
const updatedCount = await tx.paymentTransaction.updateMany({
|
||||
where: { id: transaction.id, status: 'PENDING' },
|
||||
// 1. Mark transaction as VERIFIED
|
||||
await tx.paymentTransaction.update({
|
||||
where: { id: transaction.id },
|
||||
data: {
|
||||
status: 'VERIFIED',
|
||||
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
|
||||
if (transaction.type === 'ORDER' && transaction.orderId) {
|
||||
await tx.order.update({
|
||||
@ -390,10 +330,7 @@ export class PaymentService {
|
||||
});
|
||||
|
||||
// If charity donation was made, update user's total charity
|
||||
if (
|
||||
transaction.order &&
|
||||
Number(transaction.order.charityDonation) > 0
|
||||
) {
|
||||
if (transaction.order && Number(transaction.order.charityDonation) > 0) {
|
||||
await tx.user.update({
|
||||
where: { id: transaction.userId },
|
||||
data: {
|
||||
@ -432,11 +369,8 @@ export class PaymentService {
|
||||
// Send SMS notification if order payment
|
||||
if (transaction.type === 'ORDER' && transaction.order?.user?.mobile) {
|
||||
const mobile = transaction.order.user.mobile;
|
||||
const trackingNum =
|
||||
transaction.order.trackingNumber || transaction.order.id;
|
||||
const formattedAmount = Number(transaction.amount).toLocaleString(
|
||||
'fa-IR',
|
||||
);
|
||||
const trackingNum = transaction.order.trackingNumber || transaction.order.id;
|
||||
const formattedAmount = Number(transaction.amount).toLocaleString('fa-IR');
|
||||
|
||||
this.smsService
|
||||
.sendOrderConfirmation(mobile, trackingNum, formattedAmount)
|
||||
@ -456,92 +390,20 @@ export class PaymentService {
|
||||
type: transaction.type,
|
||||
message: 'پرداخت با موفقیت انجام شد',
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.logger.error(`Error during verify: ${msg}`, stack);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error during verify: ${err.message}`, err.stack);
|
||||
return {
|
||||
success: false,
|
||||
status: 'ERROR',
|
||||
trackId,
|
||||
message: msg || 'خطا در فرایند تایید تراکنش',
|
||||
message: err.message || 'خطا در فرایند تایید تراکنش',
|
||||
orderId: transaction.orderId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. Auto-Reconciliation / Cron Job for Pending Transactions (استعلام تراکنشهای بلاتکلیف)
|
||||
*/
|
||||
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
|
||||
* 4. Get Payment Transaction Details
|
||||
*/
|
||||
async getTransaction(trackId: string) {
|
||||
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,
|
||||
createdAt: transaction.createdAt,
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,12 +115,8 @@ export class ZibalService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(
|
||||
`Zibal request HTTP error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(
|
||||
`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`,
|
||||
);
|
||||
this.logger.error(`Zibal request HTTP error ${response.status}: ${errorText}`);
|
||||
throw new Error(`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZibalRequestResponse;
|
||||
@ -146,9 +142,7 @@ export class ZibalService {
|
||||
trackId: String(trackId),
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`,
|
||||
);
|
||||
this.logger.log(`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`);
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/v1/verify`, {
|
||||
method: 'POST',
|
||||
@ -160,12 +154,8 @@ export class ZibalService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(
|
||||
`Zibal verify HTTP error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(
|
||||
`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`,
|
||||
);
|
||||
this.logger.error(`Zibal verify HTTP error ${response.status}: ${errorText}`);
|
||||
throw new Error(`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZibalVerifyResponse;
|
||||
@ -176,9 +166,7 @@ export class ZibalService {
|
||||
/**
|
||||
* 4. Inquiry Payment (استعلام وضعیت تراکنش)
|
||||
*/
|
||||
async inquiryPayment(
|
||||
trackId: string | number,
|
||||
): Promise<ZibalInquiryResponse> {
|
||||
async inquiryPayment(trackId: string | number): Promise<ZibalInquiryResponse> {
|
||||
const merchant = await this.getMerchant();
|
||||
|
||||
const payload = {
|
||||
@ -198,12 +186,8 @@ export class ZibalService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(
|
||||
`Zibal inquiry HTTP error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(
|
||||
`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`,
|
||||
);
|
||||
this.logger.error(`Zibal inquiry HTTP error ${response.status}: ${errorText}`);
|
||||
throw new Error(`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZibalInquiryResponse;
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { SettingsService, ScientificTermData } from './settings.service';
|
||||
import { SmsLogQuery } from '../common/services/sms.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
@ -23,7 +20,7 @@ import {
|
||||
ApiOkResponse,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی، سیستم و پیامک')
|
||||
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی و سیستم')
|
||||
@Controller('settings')
|
||||
export class SettingsController {
|
||||
constructor(private readonly settingsService: SettingsService) {}
|
||||
@ -128,101 +125,4 @@ export class SettingsController {
|
||||
updateSystemSettings(@Body() body: Prisma.InputJsonValue) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SmsService, SmsLogQuery } from '../common/services/sms.service';
|
||||
|
||||
export class ScientificTermData {
|
||||
term?: string;
|
||||
@ -11,10 +10,7 @@ export class ScientificTermData {
|
||||
|
||||
@Injectable()
|
||||
export class SettingsService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private smsService: SmsService,
|
||||
) {}
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getUiTexts() {
|
||||
const [uiTexts, settings] = await Promise.all([
|
||||
@ -86,45 +82,4 @@ export class SettingsService {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
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 { useAdminAuthStore } from '../store/adminAuthStore';
|
||||
|
||||
@ -16,7 +16,6 @@ const menuGroups = [
|
||||
title: 'فروشگاه و تخصصی',
|
||||
items: [
|
||||
{ icon: ShoppingCart, label: 'سفارشات', path: '/orders' },
|
||||
{ icon: Receipt, label: 'تراکنشها و لاگ پرداخت', path: '/transactions' },
|
||||
{ icon: Package, label: 'محصولات', path: '/products' },
|
||||
{ icon: FolderTree, label: 'دستهبندیها', path: '/categories' },
|
||||
{ icon: Tag, label: 'کدهای تخفیف', path: '/coupons' },
|
||||
@ -48,7 +47,6 @@ const menuGroups = [
|
||||
title: 'سیستم و تنظیمات',
|
||||
items: [
|
||||
{ icon: Settings, label: 'تنظیمات کلی', path: '/settings' },
|
||||
{ icon: MessageSquare, label: 'تنظیمات پیامک', path: '/settings/sms' },
|
||||
{ icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' },
|
||||
{ icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' },
|
||||
{ icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' },
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2, MessageSquare } from 'lucide-react';
|
||||
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2 } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
@ -107,15 +106,6 @@ export default function Settings() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">مدیریت پارامترهای اصلی و سیستمی کانینا</p>
|
||||
</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>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
);
|
||||
}
|
||||
@ -33,8 +33,6 @@ const B2BManager = lazy(() => import('../pages/B2BManager'));
|
||||
const SeoSettingsPage = lazy(() => import('../pages/SeoSettingsPage'));
|
||||
const FinancialSettingsPage = lazy(() => import('../pages/FinancialSettingsPage'));
|
||||
const SystemSettingsPage = lazy(() => import('../pages/SystemSettingsPage'));
|
||||
const SmsSettingsPage = lazy(() => import('../pages/SmsSettingsPage'));
|
||||
const Transactions = lazy(() => import('../pages/Transactions'));
|
||||
|
||||
export interface AdminRouteConfig {
|
||||
path: string;
|
||||
@ -58,10 +56,8 @@ export const router = createBrowserRouter([
|
||||
{ path: 'users/*', element: <Users /> },
|
||||
{ path: 'products/*', element: <Products /> },
|
||||
{ path: 'orders/*', element: <Orders /> },
|
||||
{ path: 'transactions/*', element: <Transactions /> },
|
||||
{ path: 'coupons/*', element: <Coupons /> },
|
||||
{ path: 'settings', element: <Settings /> },
|
||||
{ path: 'settings/sms', element: <SmsSettingsPage /> },
|
||||
{ path: 'settings/seo', element: <SeoSettingsPage /> },
|
||||
{ path: 'settings/financial', element: <FinancialSettingsPage /> },
|
||||
{ path: 'settings/system', element: <SystemSettingsPage /> },
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"0": "AdminService",
|
||||
"1": "pets/pets.controller.ts",
|
||||
"2": "Roles",
|
||||
"1": "Pet Management Controller",
|
||||
"2": "SettingsController",
|
||||
"3": "UsersService",
|
||||
"4": "CmsController",
|
||||
"5": "BannersService",
|
||||
"4": "CMS Content Management",
|
||||
"5": "OrdersService",
|
||||
"6": "auth.controller.ts",
|
||||
"7": "app.module.ts",
|
||||
"8": "CreateVideoDto",
|
||||
@ -12,32 +12,32 @@
|
||||
"10": "SmartAdvisor.tsx",
|
||||
"11": "compilerOptions",
|
||||
"12": "toPersian",
|
||||
"13": "SmsService",
|
||||
"13": "Media and Blog UI Components",
|
||||
"14": "ProductsService",
|
||||
"15": "lib/services/api.ts",
|
||||
"16": "eslint",
|
||||
"16": "devDependencies",
|
||||
"17": "useSettingsStore",
|
||||
"18": "AppModule",
|
||||
"19": "B2B Inquiry Controller",
|
||||
"20": "Category Management Controller",
|
||||
"21": "auth.service.ts",
|
||||
"22": "MediaController",
|
||||
"21": "Banner Management Controller",
|
||||
"22": "Media Upload Controller",
|
||||
"23": "Ingredient Management Controller",
|
||||
"24": "adminRoutes.tsx",
|
||||
"24": "Admin Dashboard Components",
|
||||
"25": "admin.module.ts",
|
||||
"26": "ConfirmModal.tsx",
|
||||
"26": "Pet DTOs and Controller",
|
||||
"27": "Prescription Review Controller",
|
||||
"28": "Smart Advisor Controller",
|
||||
"29": "Testimonials Management Controller",
|
||||
"30": "src/services/api.ts",
|
||||
"31": "Spinner.tsx",
|
||||
"30": "Admin Sidebar and Layout",
|
||||
"31": "Admin Settings Managers",
|
||||
"32": "useCartStore",
|
||||
"33": "ZibalService",
|
||||
"34": "main.ts",
|
||||
"35": "ContactService",
|
||||
"36": "Backend TypeScript Config",
|
||||
"37": "App TypeScript Config",
|
||||
"38": "Coupons.tsx",
|
||||
"38": "CMS and Media Modals",
|
||||
"39": "dependencies",
|
||||
"40": "Node TypeScript Config",
|
||||
"41": "Admin Blog Controller",
|
||||
@ -45,7 +45,7 @@
|
||||
"43": "devDependencies",
|
||||
"44": "Generic CRUD Controller",
|
||||
"45": "seo.module.ts",
|
||||
"46": "OrdersController",
|
||||
"46": "Coupon Management UI",
|
||||
"47": "Project Build Scripts",
|
||||
"48": "Home Data Module",
|
||||
"49": "devDependencies",
|
||||
@ -61,19 +61,19 @@
|
||||
"59": "PrismaService",
|
||||
"60": "Jest Testing Config",
|
||||
"61": "PaginationDto",
|
||||
"62": "SettingsService",
|
||||
"62": "BlogsController",
|
||||
"63": "FE-001",
|
||||
"64": "payment.module.ts",
|
||||
"64": "pagination.dto.ts",
|
||||
"65": "rules/graphify.md",
|
||||
"66": "App Health Controller",
|
||||
"67": "dependencies",
|
||||
"68": "OrdersService",
|
||||
"68": "Pet Business Logic",
|
||||
"69": "Integrity Validation Scripts",
|
||||
"70": "Admin Panel Package Config",
|
||||
"71": "Analytics and Report Charts",
|
||||
"72": "Task Orchestration Scripts",
|
||||
"73": "Backend Package Config",
|
||||
"74": "BlogsController",
|
||||
"74": "Health Log DTOs",
|
||||
"75": "React Error Boundary",
|
||||
"76": "Application Package Config",
|
||||
"77": "WikiController",
|
||||
@ -92,7 +92,7 @@
|
||||
"90": "Blog Listing Page",
|
||||
"91": "Blog Post Detail Page",
|
||||
"92": "@types/node",
|
||||
"93": "devDependencies",
|
||||
"93": "typescript",
|
||||
"94": "UI Text Seeding",
|
||||
"95": "Wiki Terms Seeding",
|
||||
"96": "Blog Management DTOs",
|
||||
@ -102,7 +102,7 @@
|
||||
"100": "Network Status Banner",
|
||||
"101": "Docker Deployment Scripts",
|
||||
"102": "DB-001",
|
||||
"103": "BlogsService",
|
||||
"103": "typescript-eslint",
|
||||
"104": "Database Migration Scripts",
|
||||
"105": "Scientific Terms Schema",
|
||||
"106": "Blog Data Seeding",
|
||||
@ -116,10 +116,10 @@
|
||||
"114": "Manifest Data Generation",
|
||||
"115": "Honest Manifest Synchronization",
|
||||
"116": "Manifest Entry Synchronization",
|
||||
"117": "WikiService",
|
||||
"117": "Pet Reminder DTOs",
|
||||
"118": "TS-001",
|
||||
"119": "TEST-001",
|
||||
"120": "ValidateCouponDto",
|
||||
"120": "@tailwindcss/postcss",
|
||||
"121": "@types/react-dom",
|
||||
"122": "Admin Panel TSConfig",
|
||||
"123": "About Page Component",
|
||||
@ -129,19 +129,19 @@
|
||||
"127": "DEVOPS-001",
|
||||
"128": "DOC-001",
|
||||
"129": "What You Must Do When Invoked",
|
||||
"130": "JwtAuthGuard",
|
||||
"130": "Roles",
|
||||
"131": "راهنمای تست سیستم (Software Testing)",
|
||||
"132": "Role & Core Objective",
|
||||
"133": "Required Review Group Closures",
|
||||
"134": "Operational Rules & Boundaries",
|
||||
"135": "Operational Rules & Boundaries",
|
||||
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||
"137": "@nestjs/cli",
|
||||
"137": "NestJS CLI Tooling",
|
||||
"138": "Operational Rules & Boundaries",
|
||||
"139": "Operational Rules & Boundaries",
|
||||
"140": "Bcrypt Type Definitions",
|
||||
"141": "bcryptjs",
|
||||
"142": "helmet",
|
||||
"141": "Passport JWT Type Definitions",
|
||||
"142": "Supertest Type Definitions",
|
||||
"143": "Blog Entity Model",
|
||||
"144": "Home Entity Model",
|
||||
"145": "Wiki Entity Model",
|
||||
@ -237,7 +237,7 @@
|
||||
"235": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
||||
"236": "Compiler Diagnostic Dispositions",
|
||||
"237": "@eslint/eslintrc",
|
||||
"238": "js-yaml",
|
||||
"238": "eslint-plugin-prettier",
|
||||
"239": "globals",
|
||||
"240": "prettier",
|
||||
"241": "prisma",
|
||||
@ -263,25 +263,5 @@
|
||||
"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",
|
||||
"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"
|
||||
"264": "tailwindcss"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,79 +1,79 @@
|
||||
{
|
||||
"0": "AdminService",
|
||||
"1": "PetsController",
|
||||
"1": "Pet Management Controller",
|
||||
"2": "Roles",
|
||||
"3": "UsersService",
|
||||
"4": "CmsController",
|
||||
"5": "BannersService",
|
||||
"6": "auth.service.ts",
|
||||
"4": "CMS Content Management",
|
||||
"5": "OrdersController",
|
||||
"6": "auth.controller.ts",
|
||||
"7": "app.module.ts",
|
||||
"8": "CreateVideoDto",
|
||||
"9": "ProductService",
|
||||
"10": "SmartAdvisor.tsx",
|
||||
"11": "compilerOptions",
|
||||
"12": "toPersian",
|
||||
"13": "pets/pets.controller.ts",
|
||||
"9": "Public Catalog Pages",
|
||||
"10": "Client Layout and Auth Modals",
|
||||
"11": "Prisma and TypeScript Config",
|
||||
"12": "Checkout and Dashboard Components",
|
||||
"13": "Media and Blog UI Components",
|
||||
"14": "ProductsService",
|
||||
"15": "lib/services/api.ts",
|
||||
"16": "devDependencies",
|
||||
"17": "useSettingsStore",
|
||||
"18": "MetricsController",
|
||||
"15": "Contact and Prescription Features",
|
||||
"16": "Development Tooling Config",
|
||||
"17": "Home Page Client Components",
|
||||
"18": "AppModule",
|
||||
"19": "B2B Inquiry Controller",
|
||||
"20": "Category Management Controller",
|
||||
"21": "RedisService",
|
||||
"22": "MediaController",
|
||||
"21": "Banner Management Controller",
|
||||
"22": "Media Upload Controller",
|
||||
"23": "Ingredient Management Controller",
|
||||
"24": "adminRoutes.tsx",
|
||||
"25": "admin.module.ts",
|
||||
"26": "api",
|
||||
"24": "Admin Dashboard Components",
|
||||
"25": "ReportsController",
|
||||
"26": "Pet DTOs and Controller",
|
||||
"27": "Prescription Review Controller",
|
||||
"28": "Smart Advisor Controller",
|
||||
"29": "Testimonials Management Controller",
|
||||
"30": "src/services/api.ts",
|
||||
"31": "Spinner.tsx",
|
||||
"32": "useCartStore",
|
||||
"33": "ZibalService",
|
||||
"30": "Admin Sidebar and Layout",
|
||||
"31": "Admin Settings Managers",
|
||||
"32": "User Profile and Success Pages",
|
||||
"33": "auth.service.ts",
|
||||
"34": "main.ts",
|
||||
"35": "payment.controller.ts",
|
||||
"35": "ContactService",
|
||||
"36": "Backend TypeScript Config",
|
||||
"37": "App TypeScript Config",
|
||||
"38": "ConfirmModal.tsx",
|
||||
"38": "CMS and Media Modals",
|
||||
"39": "dependencies",
|
||||
"40": "Node TypeScript Config",
|
||||
"41": "Admin Blog Controller",
|
||||
"42": "WholesaleService",
|
||||
"43": "devDependencies",
|
||||
"42": "WholesaleController",
|
||||
"43": "Frontend Testing and Styles",
|
||||
"44": "Generic CRUD Controller",
|
||||
"45": "seo.module.ts",
|
||||
"46": "Coupon Management UI",
|
||||
"47": "Project Build Scripts",
|
||||
"48": "Home Data Module",
|
||||
"49": "devDependencies",
|
||||
"49": "Linting and PostCSS Config",
|
||||
"50": "Database Seeding Logic",
|
||||
"51": "Prisma Database Migrations",
|
||||
"52": "dependencies",
|
||||
"52": "Frontend Core Dependencies",
|
||||
"53": "UI Skeleton and Tables",
|
||||
"54": "components/Skeleton.tsx",
|
||||
"55": "BE-001",
|
||||
"56": "VetGallery.tsx",
|
||||
"54": "Shop Loading States",
|
||||
"55": "Product Detail Page",
|
||||
"56": "Video Gallery Components",
|
||||
"57": "NPM Lifecycle Scripts",
|
||||
"58": "Pet Management API",
|
||||
"59": "PrismaService",
|
||||
"60": "Jest Testing Config",
|
||||
"61": "PaginationDto",
|
||||
"62": "WikiController",
|
||||
"63": "FE-001",
|
||||
"64": "PetsService",
|
||||
"62": "BlogsController",
|
||||
"63": "WholesaleApplyDto",
|
||||
"64": "SmsService",
|
||||
"65": "rules/graphify.md",
|
||||
"66": "App Health Controller",
|
||||
"67": "dependencies",
|
||||
"68": "OrdersService",
|
||||
"67": "Frontend UI Dependencies",
|
||||
"68": "Pet Business Logic",
|
||||
"69": "Integrity Validation Scripts",
|
||||
"70": "Admin Panel Package Config",
|
||||
"71": "Analytics and Report Charts",
|
||||
"72": "Task Orchestration Scripts",
|
||||
"73": "Backend Package Config",
|
||||
"74": "CreateHealthLogDto",
|
||||
"74": "Health Log DTOs",
|
||||
"75": "React Error Boundary",
|
||||
"76": "Application Package Config",
|
||||
"77": ".findAll",
|
||||
@ -81,7 +81,7 @@
|
||||
"79": "VPN Utility Scripts",
|
||||
"80": "NestJS CLI Config",
|
||||
"81": "Seed TypeScript Config",
|
||||
"82": "ADM-001",
|
||||
"82": "Order Business Logic",
|
||||
"83": "Browser Utility Scripts",
|
||||
"84": "Dev Server Startup",
|
||||
"85": "Architectural Audit Findings",
|
||||
@ -91,8 +91,8 @@
|
||||
"89": "Evidence Validation Scripts",
|
||||
"90": "Blog Listing Page",
|
||||
"91": "Blog Post Detail Page",
|
||||
"92": "@types/node",
|
||||
"93": "typescript",
|
||||
"92": "Node Type Definitions",
|
||||
"93": "TypeScript Language",
|
||||
"94": "UI Text Seeding",
|
||||
"95": "Wiki Terms Seeding",
|
||||
"96": "Blog Management DTOs",
|
||||
@ -101,8 +101,8 @@
|
||||
"99": "Wiki Page Routing",
|
||||
"100": "Network Status Banner",
|
||||
"101": "Docker Deployment Scripts",
|
||||
"102": "DB-001",
|
||||
"103": "CreateReminderDto",
|
||||
"102": "ESLint Core Configuration",
|
||||
"103": "TypeScript ESLint Support",
|
||||
"104": "Database Migration Scripts",
|
||||
"105": "Scientific Terms Schema",
|
||||
"106": "Blog Data Seeding",
|
||||
@ -116,29 +116,29 @@
|
||||
"114": "Manifest Data Generation",
|
||||
"115": "Honest Manifest Synchronization",
|
||||
"116": "Manifest Entry Synchronization",
|
||||
"117": "Videos.tsx",
|
||||
"118": "TS-001",
|
||||
"119": "TEST-001",
|
||||
"120": "@tailwindcss/postcss",
|
||||
"121": "@types/react-dom",
|
||||
"117": "Pet Reminder DTOs",
|
||||
"118": "React DOM Library",
|
||||
"119": "Zustand State Management",
|
||||
"120": "Tailwind PostCSS Plugin",
|
||||
"121": "React DOM Type Definitions",
|
||||
"122": "Admin Panel TSConfig",
|
||||
"123": "About Page Component",
|
||||
"124": "Privacy Page Component",
|
||||
"125": "Next.js Security Configuration",
|
||||
"126": "Typography and Font Assets",
|
||||
"127": "DEVOPS-001",
|
||||
"128": "DOC-001",
|
||||
"127": "Bcrypt Password Hashing",
|
||||
"128": "Helmet Security Middleware",
|
||||
"129": "What You Must Do When Invoked",
|
||||
"130": "RolesGuard",
|
||||
"131": "راهنمای تست سیستم (Software Testing)",
|
||||
"132": "Role & Core Objective",
|
||||
"133": "Required Review Group Closures",
|
||||
"134": "Operational Rules & Boundaries",
|
||||
"135": "Operational Rules & Boundaries",
|
||||
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||
"137": "@nestjs/cli",
|
||||
"138": "Operational Rules & Boundaries",
|
||||
"139": "Operational Rules & Boundaries",
|
||||
"130": "NestJS Core Framework",
|
||||
"131": "NestJS JWT Authentication",
|
||||
"132": "NestJS Swagger Documentation",
|
||||
"133": "NestJS Rate Limiting",
|
||||
"134": "Passport JWT Strategy",
|
||||
"135": "Prisma Client Library",
|
||||
"136": "Swagger UI Express",
|
||||
"137": "NestJS CLI Tooling",
|
||||
"138": "NestJS Testing Utilities",
|
||||
"139": "TypeScript Jest Integration",
|
||||
"140": "Bcrypt Type Definitions",
|
||||
"141": "Passport JWT Type Definitions",
|
||||
"142": "Supertest Type Definitions",
|
||||
@ -176,7 +176,7 @@
|
||||
"174": "Production Docker Setup",
|
||||
"175": "Staging Docker Setup",
|
||||
"176": ".agents/workflows/graphify.md",
|
||||
"177": "Role & Core Objective",
|
||||
"177": "BlogsService",
|
||||
"178": "graphify reference: extra exports and benchmark",
|
||||
"179": "graphify reference: query, path, explain",
|
||||
"180": "graphify reference: add a URL and watch a folder",
|
||||
@ -184,87 +184,12 @@
|
||||
"182": "graphify reference: incremental update and cluster-only",
|
||||
"183": "graphify reference: GitHub clone and cross-repo merge",
|
||||
"184": "graphify reference: transcribe video and audio",
|
||||
"185": "Reconciled Audit Roles & Assignments",
|
||||
"185": "AGENTS.md",
|
||||
"186": "instructions.md",
|
||||
"187": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
||||
"187": "js-yaml",
|
||||
"188": "CLAUDE.md",
|
||||
"189": ".claude/CLAUDE.md",
|
||||
"190": "extraction-spec.md",
|
||||
"191": "User Login API",
|
||||
"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"
|
||||
"192": "Developer Standards and Architecture"
|
||||
}
|
||||
|
||||
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
@ -1,26 +1,26 @@
|
||||
# Graph Report - canina (2026-08-16)
|
||||
|
||||
## Corpus Check
|
||||
- 463 files · ~678,911 words
|
||||
- 459 files · ~667,716 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 3234 nodes · 5269 edges · 285 communities (172 shown, 113 thin omitted)
|
||||
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 186 edges (avg confidence: 0.79)
|
||||
- 3167 nodes · 5051 edges · 265 communities (171 shown, 94 thin omitted)
|
||||
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 160 edges (avg confidence: 0.79)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## 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 `graphify update .` after code changes (no API cost).
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- AdminService
|
||||
- pets/pets.controller.ts
|
||||
- Roles
|
||||
- Pet Management Controller
|
||||
- SettingsController
|
||||
- UsersService
|
||||
- CmsController
|
||||
- BannersService
|
||||
- CMS Content Management
|
||||
- OrdersService
|
||||
- auth.controller.ts
|
||||
- app.module.ts
|
||||
- CreateVideoDto
|
||||
@ -28,32 +28,32 @@
|
||||
- SmartAdvisor.tsx
|
||||
- compilerOptions
|
||||
- toPersian
|
||||
- SmsService
|
||||
- Media and Blog UI Components
|
||||
- ProductsService
|
||||
- lib/services/api.ts
|
||||
- eslint
|
||||
- devDependencies
|
||||
- useSettingsStore
|
||||
- AppModule
|
||||
- B2B Inquiry Controller
|
||||
- Category Management Controller
|
||||
- auth.service.ts
|
||||
- MediaController
|
||||
- Banner Management Controller
|
||||
- Media Upload Controller
|
||||
- Ingredient Management Controller
|
||||
- adminRoutes.tsx
|
||||
- Admin Dashboard Components
|
||||
- admin.module.ts
|
||||
- ConfirmModal.tsx
|
||||
- Pet DTOs and Controller
|
||||
- Prescription Review Controller
|
||||
- Smart Advisor Controller
|
||||
- Testimonials Management Controller
|
||||
- src/services/api.ts
|
||||
- Spinner.tsx
|
||||
- Admin Sidebar and Layout
|
||||
- Admin Settings Managers
|
||||
- useCartStore
|
||||
- ZibalService
|
||||
- main.ts
|
||||
- ContactService
|
||||
- Backend TypeScript Config
|
||||
- App TypeScript Config
|
||||
- Coupons.tsx
|
||||
- CMS and Media Modals
|
||||
- dependencies
|
||||
- Node TypeScript Config
|
||||
- Admin Blog Controller
|
||||
@ -61,7 +61,7 @@
|
||||
- devDependencies
|
||||
- Generic CRUD Controller
|
||||
- seo.module.ts
|
||||
- OrdersController
|
||||
- Coupon Management UI
|
||||
- Project Build Scripts
|
||||
- Home Data Module
|
||||
- devDependencies
|
||||
@ -77,19 +77,19 @@
|
||||
- PrismaService
|
||||
- Jest Testing Config
|
||||
- PaginationDto
|
||||
- SettingsService
|
||||
- BlogsController
|
||||
- FE-001
|
||||
- payment.module.ts
|
||||
- pagination.dto.ts
|
||||
- rules/graphify.md
|
||||
- App Health Controller
|
||||
- dependencies
|
||||
- OrdersService
|
||||
- Pet Business Logic
|
||||
- Integrity Validation Scripts
|
||||
- Admin Panel Package Config
|
||||
- Analytics and Report Charts
|
||||
- Task Orchestration Scripts
|
||||
- Backend Package Config
|
||||
- BlogsController
|
||||
- Health Log DTOs
|
||||
- React Error Boundary
|
||||
- Application Package Config
|
||||
- WikiController
|
||||
@ -108,7 +108,7 @@
|
||||
- Blog Listing Page
|
||||
- Blog Post Detail Page
|
||||
- @types/node
|
||||
- devDependencies
|
||||
- typescript
|
||||
- UI Text Seeding
|
||||
- Wiki Terms Seeding
|
||||
- Blog Management DTOs
|
||||
@ -118,7 +118,7 @@
|
||||
- Network Status Banner
|
||||
- Docker Deployment Scripts
|
||||
- DB-001
|
||||
- BlogsService
|
||||
- typescript-eslint
|
||||
- Database Migration Scripts
|
||||
- Scientific Terms Schema
|
||||
- Blog Data Seeding
|
||||
@ -132,10 +132,10 @@
|
||||
- Manifest Data Generation
|
||||
- Honest Manifest Synchronization
|
||||
- Manifest Entry Synchronization
|
||||
- WikiService
|
||||
- Pet Reminder DTOs
|
||||
- TS-001
|
||||
- TEST-001
|
||||
- ValidateCouponDto
|
||||
- @tailwindcss/postcss
|
||||
- @types/react-dom
|
||||
- Admin Panel TSConfig
|
||||
- About Page Component
|
||||
@ -145,19 +145,19 @@
|
||||
- DEVOPS-001
|
||||
- DOC-001
|
||||
- What You Must Do When Invoked
|
||||
- JwtAuthGuard
|
||||
- Roles
|
||||
- راهنمای تست سیستم (Software Testing)
|
||||
- Role & Core Objective
|
||||
- Required Review Group Closures
|
||||
- Operational Rules & Boundaries
|
||||
- Operational Rules & Boundaries
|
||||
- 🏢 AI Software Agency — Master Orchestration Protocol v3
|
||||
- @nestjs/cli
|
||||
- NestJS CLI Tooling
|
||||
- Operational Rules & Boundaries
|
||||
- Operational Rules & Boundaries
|
||||
- Bcrypt Type Definitions
|
||||
- bcryptjs
|
||||
- helmet
|
||||
- Passport JWT Type Definitions
|
||||
- Supertest Type Definitions
|
||||
- Blog Entity Model
|
||||
- Home Entity Model
|
||||
- Wiki Entity Model
|
||||
@ -246,7 +246,7 @@
|
||||
- 👁️ UX & Persona Interface Review (08_visual_qa)
|
||||
- Compiler Diagnostic Dispositions
|
||||
- @eslint/eslintrc
|
||||
- js-yaml
|
||||
- eslint-plugin-prettier
|
||||
- globals
|
||||
- prettier
|
||||
- prisma
|
||||
@ -266,38 +266,18 @@
|
||||
- @types/react
|
||||
- typescript
|
||||
- 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)
|
||||
1. `PrismaService` - 74 edges
|
||||
2. `Roles()` - 59 edges
|
||||
1. `PrismaService` - 72 edges
|
||||
2. `Roles()` - 45 edges
|
||||
3. `PaginationDto` - 39 edges
|
||||
4. `SmsService` - 38 edges
|
||||
5. `api` - 34 edges
|
||||
6. `useSettingsStore` - 33 edges
|
||||
7. `AdminService` - 31 edges
|
||||
8. `toPersian()` - 31 edges
|
||||
9. `AdminController` - 30 edges
|
||||
10. `useCartStore` - 29 edges
|
||||
4. `useSettingsStore` - 33 edges
|
||||
5. `api` - 32 edges
|
||||
6. `AdminService` - 31 edges
|
||||
7. `toPersian()` - 31 edges
|
||||
8. `AdminController` - 30 edges
|
||||
9. `useCartStore` - 29 edges
|
||||
10. `JwtAuthGuard` - 27 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `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
|
||||
|
||||
## 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/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`
|
||||
|
||||
## 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]
|
||||
- **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"
|
||||
Cohesion: 0.07
|
||||
Nodes (22): AdminController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+14 more)
|
||||
|
||||
### Community 1 - "pets/pets.controller.ts"
|
||||
Cohesion: 0.05
|
||||
Nodes (45): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+37 more)
|
||||
### Community 1 - "Pet Management Controller"
|
||||
Cohesion: 0.17
|
||||
Nodes (18): PetsController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+10 more)
|
||||
|
||||
### Community 2 - "Roles"
|
||||
Cohesion: 0.19
|
||||
Nodes (16): Roles(), SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+8 more)
|
||||
### Community 2 - "SettingsController"
|
||||
Cohesion: 0.13
|
||||
Nodes (16): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+8 more)
|
||||
|
||||
### Community 3 - "UsersService"
|
||||
Cohesion: 0.07
|
||||
Nodes (33): JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty (+25 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (37): AuthModule, Module, JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional (+29 more)
|
||||
|
||||
### Community 4 - "CmsController"
|
||||
### Community 4 - "CMS Content Management"
|
||||
Cohesion: 0.09
|
||||
Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
|
||||
|
||||
### Community 5 - "BannersService"
|
||||
Cohesion: 0.13
|
||||
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
|
||||
### Community 5 - "OrdersService"
|
||||
Cohesion: 0.07
|
||||
Nodes (29): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+21 more)
|
||||
|
||||
### Community 6 - "auth.controller.ts"
|
||||
Cohesion: 0.07
|
||||
Nodes (39): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+31 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (41): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+33 more)
|
||||
|
||||
### Community 7 - "app.module.ts"
|
||||
Cohesion: 0.10
|
||||
Nodes (25): AuthModule, Module, B2BModule, Module, BannersModule, Module, CmsModule, Module (+17 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (28): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+20 more)
|
||||
|
||||
### Community 8 - "CreateVideoDto"
|
||||
Cohesion: 0.07
|
||||
@ -374,6 +354,10 @@ Nodes (32): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModu
|
||||
Cohesion: 0.08
|
||||
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"
|
||||
Cohesion: 0.09
|
||||
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
|
||||
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"
|
||||
Cohesion: 0.07
|
||||
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"
|
||||
Cohesion: 0.13
|
||||
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
|
||||
Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more)
|
||||
|
||||
### Community 21 - "auth.service.ts"
|
||||
Cohesion: 0.10
|
||||
Nodes (10): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, RedisModule, Global, Module (+2 more)
|
||||
### Community 21 - "Banner Management Controller"
|
||||
Cohesion: 0.13
|
||||
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
|
||||
|
||||
### Community 22 - "MediaController"
|
||||
Cohesion: 0.10
|
||||
### Community 22 - "Media Upload Controller"
|
||||
Cohesion: 0.11
|
||||
Nodes (16): MediaController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
|
||||
|
||||
### Community 23 - "Ingredient Management Controller"
|
||||
Cohesion: 0.13
|
||||
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
||||
|
||||
### Community 24 - "adminRoutes.tsx"
|
||||
Cohesion: 0.08
|
||||
Nodes (18): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, PatternItem, SmsConfigState, SmsLogItem (+10 more)
|
||||
### Community 24 - "Admin Dashboard Components"
|
||||
Cohesion: 0.10
|
||||
Nodes (14): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, GROUP_PAGE_MAP, GROUPS, PAGE_TABS (+6 more)
|
||||
|
||||
### Community 25 - "admin.module.ts"
|
||||
Cohesion: 0.10
|
||||
Nodes (14): AdminModule, Module, PetQuery, PetsService, Injectable, ReportsController, ApiBearerAuth, ApiOperation (+6 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (20): AdminModule, Module, BlogQuery, BlogsService, Injectable, PetQuery, PetsService, Injectable (+12 more)
|
||||
|
||||
### Community 26 - "ConfirmModal.tsx"
|
||||
Cohesion: 0.10
|
||||
Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Pet, fetchVideosList(), Video, Videos() (+8 more)
|
||||
### Community 26 - "Pet DTOs and Controller"
|
||||
Cohesion: 0.16
|
||||
Nodes (13): CreatePetDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional, IsString (+5 more)
|
||||
|
||||
### Community 27 - "Prescription Review Controller"
|
||||
Cohesion: 0.14
|
||||
@ -430,21 +422,21 @@ Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body,
|
||||
Cohesion: 0.13
|
||||
Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
||||
|
||||
### Community 30 - "src/services/api.ts"
|
||||
Cohesion: 0.10
|
||||
Nodes (27): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+19 more)
|
||||
### Community 30 - "Admin Sidebar and Layout"
|
||||
Cohesion: 0.17
|
||||
Nodes (16): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+8 more)
|
||||
|
||||
### Community 31 - "Spinner.tsx"
|
||||
Cohesion: 0.08
|
||||
Nodes (23): Media, MediaSelector(), MediaSelectorProps, Spinner(), BlogPost, Category, Category, Product (+15 more)
|
||||
### Community 31 - "Admin Settings Managers"
|
||||
Cohesion: 0.09
|
||||
Nodes (21): Spinner(), B2BManager, BannersManager, FinancialSettingsPage, SeoSettingsPage, SmartAdvisorManager, SystemSettingsPage, TestimonialsManager (+13 more)
|
||||
|
||||
### Community 32 - "useCartStore"
|
||||
Cohesion: 0.12
|
||||
Nodes (16): metadata, ArchiveProductCard(), ProductCard(), Header(), MENU_ICONS, OrderSuccess(), OrderTracking(), PetProfile() (+8 more)
|
||||
|
||||
### Community 33 - "ZibalService"
|
||||
Cohesion: 0.06
|
||||
Nodes (37): AdminTransactionFilterDto, ApiPropertyOptional, IsNumber, IsOptional, IsString, Type, InitiatePaymentDto, InitiateWalletTopupDto (+29 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (30): InitiatePaymentDto, InitiateWalletTopupDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+22 more)
|
||||
|
||||
### Community 34 - "main.ts"
|
||||
Cohesion: 0.14
|
||||
@ -462,13 +454,13 @@ Nodes (22): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration,
|
||||
Cohesion: 0.09
|
||||
Nodes (22): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+14 more)
|
||||
|
||||
### Community 38 - "Coupons.tsx"
|
||||
Cohesion: 0.09
|
||||
Nodes (15): Pagination(), PaginationProps, Coupon, CouponFormData, CouponModalProps, CouponTarget, Media, Stats (+7 more)
|
||||
### Community 38 - "CMS and Media Modals"
|
||||
Cohesion: 0.10
|
||||
Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Media, fetchVideosList(), Video, Videos() (+8 more)
|
||||
|
||||
### Community 39 - "dependencies"
|
||||
Cohesion: 0.10
|
||||
Nodes (21): dependencies, bcrypt, class-transformer, class-validator, ioredis, @nestjs/common, @nestjs/passport, @nestjs/platform-express (+13 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (41): dependencies, bcrypt, bcryptjs, class-transformer, class-validator, helmet, ioredis, js-yaml (+33 more)
|
||||
|
||||
### Community 40 - "Node TypeScript Config"
|
||||
Cohesion: 0.10
|
||||
@ -494,9 +486,9 @@ Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, De
|
||||
Cohesion: 0.16
|
||||
Nodes (10): SeoController, ApiOperation, ApiTags, Controller, Get, Param, SeoModule, Module (+2 more)
|
||||
|
||||
### Community 46 - "OrdersController"
|
||||
Cohesion: 0.13
|
||||
Nodes (16): OrdersController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+8 more)
|
||||
### Community 46 - "Coupon Management UI"
|
||||
Cohesion: 0.25
|
||||
Nodes (5): Coupon, CouponFormData, CouponModalProps, CouponTarget, Coupons
|
||||
|
||||
### Community 47 - "Project Build Scripts"
|
||||
Cohesion: 0.11
|
||||
@ -508,7 +500,7 @@ Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Ge
|
||||
|
||||
### Community 49 - "devDependencies"
|
||||
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"
|
||||
Cohesion: 0.17
|
||||
@ -547,8 +539,8 @@ Cohesion: 0.15
|
||||
Nodes (11): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+3 more)
|
||||
|
||||
### Community 59 - "PrismaService"
|
||||
Cohesion: 0.08
|
||||
Nodes (18): ApiExcludeController, CouponTargetInput, PaginationQuery, CategoryQuery, MetricsController, Controller, Get, Res (+10 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (21): CouponTargetInput, PaginationQuery, CategoryQuery, AdminLoginInput, LoginInput, RegisterInput, MeliPayamakResponse, SendPatternSmsOptions (+13 more)
|
||||
|
||||
### Community 60 - "Jest Testing Config"
|
||||
Cohesion: 0.15
|
||||
@ -556,19 +548,19 @@ Nodes (13): jest, collectCoverageFrom, coverageDirectory, moduleFileExtensions,
|
||||
|
||||
### Community 61 - "PaginationDto"
|
||||
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"
|
||||
Cohesion: 0.11
|
||||
Nodes (4): SmsLogQuery, ScientificTermData, SettingsService, Injectable
|
||||
### Community 62 - "BlogsController"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable
|
||||
|
||||
### Community 63 - "FE-001"
|
||||
Cohesion: 0.06
|
||||
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"
|
||||
Cohesion: 0.16
|
||||
Nodes (10): SmsModule, Global, Module, ContactModule, Module, PaymentModule, Module, ZibalInquiryResponse (+2 more)
|
||||
### Community 64 - "pagination.dto.ts"
|
||||
Cohesion: 0.11
|
||||
Nodes (10): Injectable, WikiQuery, WikiService, SortOrder, IsNotEmpty, IsNumber, IsString, ValidateCouponDto (+2 more)
|
||||
|
||||
### Community 66 - "App Health Controller"
|
||||
Cohesion: 0.29
|
||||
@ -578,10 +570,6 @@ Nodes (5): AppController, Controller, Get, AppService, Injectable
|
||||
Cohesion: 0.12
|
||||
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"
|
||||
Cohesion: 0.20
|
||||
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
|
||||
Nodes (8): author, description, license, name, prisma, seed, private, version
|
||||
|
||||
### Community 74 - "BlogsController"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable
|
||||
### Community 74 - "Health Log DTOs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
|
||||
|
||||
### Community 75 - "React Error Boundary"
|
||||
Cohesion: 0.22
|
||||
@ -666,10 +654,6 @@ Nodes (4): Blog(), getBlogs(), metadata, BlogPage()
|
||||
Cohesion: 0.60
|
||||
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"
|
||||
Cohesion: 0.83
|
||||
Nodes (3): generateMetadata(), getWikiTerm(), WikiTermPage()
|
||||
@ -682,17 +666,13 @@ Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script
|
||||
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)
|
||||
|
||||
### Community 103 - "BlogsService"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): BlogQuery, BlogsService, Injectable
|
||||
|
||||
### Community 109 - "Auth Architecture and Planning"
|
||||
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
|
||||
|
||||
### Community 117 - "WikiService"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): Injectable, WikiQuery, WikiService
|
||||
### Community 117 - "Pet Reminder DTOs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): CreateReminderDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
|
||||
|
||||
### Community 118 - "TS-001"
|
||||
Cohesion: 0.06
|
||||
@ -702,10 +682,6 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
|
||||
Cohesion: 0.06
|
||||
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"
|
||||
Cohesion: 0.67
|
||||
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
|
||||
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"
|
||||
Cohesion: 0.18
|
||||
Nodes (7): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
|
||||
### Community 130 - "Roles"
|
||||
Cohesion: 0.21
|
||||
Nodes (8): JwtAuthGuard, Injectable, Roles(), ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
|
||||
|
||||
### Community 131 - "راهنمای تست سیستم (Software Testing)"
|
||||
Cohesion: 0.07
|
||||
@ -955,24 +931,24 @@ Cohesion: 0.50
|
||||
Nodes (3): Deploy on Vercel, Getting Started, Learn More
|
||||
|
||||
## 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.
|
||||
- **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
|
||||
_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`?**
|
||||
_High betweenness centrality (0.039) - 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`?**
|
||||
_High betweenness centrality (0.037) - 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`?**
|
||||
_High betweenness centrality (0.022) - this node is a cross-community bridge._
|
||||
- **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.033) - this node is a cross-community bridge._
|
||||
- **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.020) - this node is a cross-community bridge._
|
||||
- **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.020) - this node is a cross-community bridge._
|
||||
- **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?**
|
||||
_Cohesion score 0.06841046277665996 - nodes in this community are weakly interconnected._
|
||||
- **Should `pets/pets.controller.ts` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.053923541247484906 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.06690140845070422 - nodes in this community are weakly interconnected._
|
||||
- **Should `SettingsController` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.1251778093883357 - nodes in this community are weakly interconnected._
|
||||
- **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._
|
||||
@ -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}
|
||||
2
graphify-out/cache/stat-index.json
vendored
2
graphify-out/cache/stat-index.json
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11796
graphify-out/graph.json
11796
graphify-out/graph.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user