Compare commits
No commits in common. "5ace474d71f8132ffe015e56fda81fed9849076e" and "8992060cb74cb3647ecdbf06d6437ecd23d9313f" have entirely different histories.
5ace474d71
...
8992060cb7
@ -583,23 +583,3 @@ model Setting {
|
|||||||
@@index([category])
|
@@index([category])
|
||||||
@@map("settings")
|
@@map("settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
model SmsLog {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
receptor String @db.VarChar(20)
|
|
||||||
type String @db.VarChar(50) // OTP, ORDER_CONFIRMATION, SHIPPING_TRACKING, B2B_NOTIFICATION, PET_CARE_REMINDER, TEST, GENERIC
|
|
||||||
patternId Int? @map("pattern_id")
|
|
||||||
args String[] @default([])
|
|
||||||
messageText String? @map("message_text") @db.Text
|
|
||||||
status String @db.VarChar(20) // SUCCESS, FAILED, DISABLED
|
|
||||||
recId String? @map("rec_id") @db.VarChar(50)
|
|
||||||
errorMessage String? @map("error_message") @db.Text
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
|
||||||
|
|
||||||
@@index([receptor])
|
|
||||||
@@index([type])
|
|
||||||
@@index([status])
|
|
||||||
@@index([createdAt])
|
|
||||||
@@map("sms_logs")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,6 @@ import { PrescriptionsModule } from './prescriptions/prescriptions.module';
|
|||||||
import { B2BModule } from './b2b/b2b.module';
|
import { B2BModule } from './b2b/b2b.module';
|
||||||
import { PaymentModule } from './payment/payment.module';
|
import { PaymentModule } from './payment/payment.module';
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
SmsModule,
|
SmsModule,
|
||||||
@ -79,18 +77,15 @@ export class AppModule implements NestModule {
|
|||||||
|
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer
|
consumer
|
||||||
.apply((req: Request, res: Response, next: NextFunction) => {
|
.apply((req: any, res: any, next: () => void) => {
|
||||||
MetricsController.incrementRequestCount();
|
MetricsController.incrementRequestCount();
|
||||||
|
|
||||||
const url: string = req.originalUrl || req.url || '';
|
const url: string = req.originalUrl || req.url || '';
|
||||||
const method: string = req.method || '';
|
const method: string = req.method || '';
|
||||||
|
|
||||||
// Exclude options, admin panel requests, static assets, and health metrics from visit counter
|
// Exclude options, admin panel requests, static assets, and health metrics from visit counter
|
||||||
const isAdminPath =
|
const isAdminPath = url.includes('/admin') || url.includes('/api/admin');
|
||||||
url.includes('/admin') || url.includes('/api/admin');
|
const isStaticAsset = url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i);
|
||||||
const isStaticAsset = url.match(
|
|
||||||
/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$/i,
|
|
||||||
);
|
|
||||||
const isIgnoredMethod = method === 'OPTIONS' || method === 'HEAD';
|
const isIgnoredMethod = method === 'OPTIONS' || method === 'HEAD';
|
||||||
|
|
||||||
if (!isAdminPath && !isStaticAsset && !isIgnoredMethod) {
|
if (!isAdminPath && !isStaticAsset && !isIgnoredMethod) {
|
||||||
|
|||||||
@ -49,8 +49,7 @@ export class AuthService {
|
|||||||
// Remove OTP from Redis if SMS failed to avoid phantom codes
|
// Remove OTP from Redis if SMS failed to avoid phantom codes
|
||||||
await this.redisService.del(`otp:${phoneNumber}`);
|
await this.redisService.del(`otp:${phoneNumber}`);
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message:
|
message: 'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
|
||||||
'ارسال پیامک با خطا مواجه شد. لطفاً چند لحظه دیگر دوباره تلاش کنید.',
|
|
||||||
error: 'SMS_DELIVERY_FAILED',
|
error: 'SMS_DELIVERY_FAILED',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,646 +1,40 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import { PrismaService } from '../../prisma/prisma.service';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface SendPatternSmsOptions {
|
export interface SendPatternSmsOptions {
|
||||||
to: string;
|
to: string;
|
||||||
bodyId: number; // MeliPayamak Shared Pattern Body ID
|
bodyId: number; // MeliPayamak Shared Pattern Body ID
|
||||||
args: string[]; // Dynamic variables inside pattern
|
args: string[]; // Dynamic variables inside pattern
|
||||||
type?: string; // OTP, ORDER_CONFIRMATION, SHIPPING_TRACKING, B2B_NOTIFICATION, PET_CARE_REMINDER, TEST, GENERIC
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmsConfig {
|
interface MeliPayamakResponse {
|
||||||
enabled: boolean;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
fromNumber?: string;
|
|
||||||
otpBodyId: number;
|
|
||||||
orderBodyId: number;
|
|
||||||
shippingBodyId: number;
|
|
||||||
b2bBodyId: number;
|
|
||||||
petCareBodyId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MeliPayamakPattern {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
status: number; // 0 = Pending, 1 = Approved, 2 = Needs Edit
|
|
||||||
statusText?: string;
|
|
||||||
assignedTo?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SmsLogQuery {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
search?: string;
|
|
||||||
type?: string;
|
|
||||||
status?: string;
|
|
||||||
sortBy?: string;
|
|
||||||
sortOrder?: 'asc' | 'desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MeliPayamakResponse {
|
|
||||||
Value?: number;
|
Value?: number;
|
||||||
RetStatus?: number;
|
RetStatus?: number;
|
||||||
StrRetStatus?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmsService {
|
export class SmsService {
|
||||||
private readonly logger = new Logger(SmsService.name);
|
private readonly logger = new Logger(SmsService.name);
|
||||||
|
private readonly username = process.env.MELIPAYAMAK_USERNAME || '9364100228';
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
private readonly password = process.env.MELIPAYAMAK_PASSWORD || '';
|
||||||
|
|
||||||
/**
|
|
||||||
* Get active SMS configuration from database (fallback to .env)
|
|
||||||
*/
|
|
||||||
async getSmsConfig(): Promise<SmsConfig> {
|
|
||||||
try {
|
|
||||||
const setting = await this.prisma.setting.findFirst({
|
|
||||||
where: { category: 'sms' },
|
|
||||||
});
|
|
||||||
const dbConfig = (setting?.value as Record<string, unknown>) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled:
|
|
||||||
dbConfig.enabled !== undefined ? Boolean(dbConfig.enabled) : true,
|
|
||||||
username:
|
|
||||||
typeof dbConfig.username === 'string' && dbConfig.username
|
|
||||||
? dbConfig.username
|
|
||||||
: process.env.MELIPAYAMAK_USERNAME || '9364100228',
|
|
||||||
password:
|
|
||||||
typeof dbConfig.password === 'string' && dbConfig.password
|
|
||||||
? dbConfig.password
|
|
||||||
: process.env.MELIPAYAMAK_PASSWORD || '',
|
|
||||||
fromNumber:
|
|
||||||
typeof dbConfig.fromNumber === 'string' && dbConfig.fromNumber
|
|
||||||
? dbConfig.fromNumber
|
|
||||||
: process.env.MELIPAYAMAK_FROM_NUMBER || '',
|
|
||||||
otpBodyId: Number(
|
|
||||||
dbConfig.otpBodyId || process.env.MELIPAYAMAK_OTP_BODY_ID || '508079',
|
|
||||||
),
|
|
||||||
orderBodyId: Number(
|
|
||||||
dbConfig.orderBodyId ||
|
|
||||||
process.env.MELIPAYAMAK_ORDER_BODY_ID ||
|
|
||||||
'508081',
|
|
||||||
),
|
|
||||||
shippingBodyId: Number(
|
|
||||||
dbConfig.shippingBodyId ||
|
|
||||||
process.env.MELIPAYAMAK_SHIPPING_BODY_ID ||
|
|
||||||
'508082',
|
|
||||||
),
|
|
||||||
b2bBodyId: Number(
|
|
||||||
dbConfig.b2bBodyId || process.env.MELIPAYAMAK_B2B_BODY_ID || '508083',
|
|
||||||
),
|
|
||||||
petCareBodyId: Number(
|
|
||||||
dbConfig.petCareBodyId ||
|
|
||||||
process.env.MELIPAYAMAK_PET_CARE_BODY_ID ||
|
|
||||||
'0',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
username: process.env.MELIPAYAMAK_USERNAME || '9364100228',
|
|
||||||
password: process.env.MELIPAYAMAK_PASSWORD || '',
|
|
||||||
fromNumber: process.env.MELIPAYAMAK_FROM_NUMBER || '',
|
|
||||||
otpBodyId: Number(process.env.MELIPAYAMAK_OTP_BODY_ID || '508079'),
|
|
||||||
orderBodyId: Number(process.env.MELIPAYAMAK_ORDER_BODY_ID || '508081'),
|
|
||||||
shippingBodyId: Number(
|
|
||||||
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '508082',
|
|
||||||
),
|
|
||||||
b2bBodyId: Number(process.env.MELIPAYAMAK_B2B_BODY_ID || '508083'),
|
|
||||||
petCareBodyId: Number(process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper HTTP POST request to Payamak-Panel ASMX endpoints
|
|
||||||
*/
|
|
||||||
private async postAsmx(
|
|
||||||
endpoint: string,
|
|
||||||
params: Record<string, string | number>,
|
|
||||||
): Promise<string> {
|
|
||||||
const postData = Object.entries(params)
|
|
||||||
.map(
|
|
||||||
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`,
|
|
||||||
)
|
|
||||||
.join('&');
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = https.request(
|
|
||||||
`https://api.payamak-panel.com/post/SharedService.asmx/${endpoint}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': Buffer.byteLength(postData),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
|
||||||
res.on('end', () => resolve(data));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
req.on('error', (err: Error) => reject(err));
|
|
||||||
req.write(postData);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to write an SMS Log to PostgreSQL safely
|
|
||||||
*/
|
|
||||||
private async recordLog(entry: {
|
|
||||||
receptor: string;
|
|
||||||
type: string;
|
|
||||||
patternId?: number;
|
|
||||||
args?: string[];
|
|
||||||
messageText?: string;
|
|
||||||
status: string;
|
|
||||||
recId?: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.prisma.smsLog.create({
|
|
||||||
data: {
|
|
||||||
receptor: entry.receptor,
|
|
||||||
type: entry.type || 'GENERIC',
|
|
||||||
patternId: entry.patternId || null,
|
|
||||||
args: entry.args || [],
|
|
||||||
messageText: entry.messageText || null,
|
|
||||||
status: entry.status,
|
|
||||||
recId: entry.recId ? String(entry.recId) : null,
|
|
||||||
errorMessage: entry.errorMessage || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
this.logger.error(`[SMS Log Save Failed]: ${msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query paginated SMS Logs with Search, Filter and Sorting
|
|
||||||
*/
|
|
||||||
async getSmsLogs(query: SmsLogQuery = {}) {
|
|
||||||
const page = Math.max(1, Number(query.page) || 1);
|
|
||||||
const limit = Math.max(1, Math.min(100, Number(query.limit) || 15));
|
|
||||||
const skip = (page - 1) * limit;
|
|
||||||
|
|
||||||
const where: Prisma.SmsLogWhereInput = {};
|
|
||||||
|
|
||||||
if (query.search) {
|
|
||||||
const s = query.search.trim();
|
|
||||||
where.OR = [
|
|
||||||
{ receptor: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ recId: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ errorMessage: { contains: s, mode: 'insensitive' } },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.type && query.type !== 'ALL') {
|
|
||||||
where.type = query.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.status && query.status !== 'ALL') {
|
|
||||||
where.status = query.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortField = query.sortBy || 'createdAt';
|
|
||||||
const sortOrder = query.sortOrder === 'asc' ? 'asc' : 'desc';
|
|
||||||
|
|
||||||
const [logs, total, totalSuccess, totalFailed] = await Promise.all([
|
|
||||||
this.prisma.smsLog.findMany({
|
|
||||||
where,
|
|
||||||
skip,
|
|
||||||
take: limit,
|
|
||||||
orderBy: { [sortField]: sortOrder },
|
|
||||||
}),
|
|
||||||
this.prisma.smsLog.count({ where }),
|
|
||||||
this.prisma.smsLog.count({ where: { status: 'SUCCESS' } }),
|
|
||||||
this.prisma.smsLog.count({ where: { status: 'FAILED' } }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
logs,
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(total / limit),
|
|
||||||
stats: {
|
|
||||||
total,
|
|
||||||
success: totalSuccess,
|
|
||||||
failed: totalFailed,
|
|
||||||
successRate: total > 0 ? Math.round((totalSuccess / total) * 100) : 100,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete specific SMS log entry
|
|
||||||
*/
|
|
||||||
async deleteSmsLog(id: string) {
|
|
||||||
return this.prisma.smsLog.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all SMS logs
|
|
||||||
*/
|
|
||||||
async clearAllSmsLogs() {
|
|
||||||
return this.prisma.smsLog.deleteMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse XML response for GetSharedServiceBody into structured pattern array
|
|
||||||
*/
|
|
||||||
private parsePatternsXml(xml: string): MeliPayamakPattern[] {
|
|
||||||
const patterns: MeliPayamakPattern[] = [];
|
|
||||||
const itemRegex = /<SharedServiceBody>([\s\S]*?)<\/SharedServiceBody>/gi;
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((match = itemRegex.exec(xml)) !== null) {
|
|
||||||
const block = match[1];
|
|
||||||
const idMatch = block.match(/<Id>(\d+)<\/Id>/i);
|
|
||||||
const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
|
|
||||||
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i);
|
|
||||||
const statusMatch = block.match(/<Status>(-?\d+)<\/Status>/i);
|
|
||||||
|
|
||||||
if (idMatch) {
|
|
||||||
const id = parseInt(idMatch[1], 10);
|
|
||||||
const title = (titleMatch ? titleMatch[1] : '')
|
|
||||||
.replace(/</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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Pattern SMS using MeliPayamak Shared Service Line (Bypasses Blacklist)
|
* Send Pattern SMS using MeliPayamak Shared Service Line (Bypasses Blacklist)
|
||||||
*/
|
*/
|
||||||
async sendPatternSms(options: SendPatternSmsOptions): Promise<boolean> {
|
async sendPatternSms(options: SendPatternSmsOptions): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
if (!this.username || !this.password) {
|
||||||
const msgType = options.type || 'GENERIC_PATTERN';
|
this.logger.error(
|
||||||
|
`[SMS MISCONFIGURED] MELIPAYAMAK_PASSWORD is not set! ` +
|
||||||
if (!config.enabled) {
|
`SMS to ${options.to} (Pattern: ${options.bodyId}) was NOT sent. ` +
|
||||||
this.logger.warn(`[SMS Disabled] SMS dispatch skipped for ${options.to}`);
|
`Set MELIPAYAMAK_USERNAME and MELIPAYAMAK_PASSWORD environment variables.`,
|
||||||
await this.recordLog({
|
);
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'DISABLED',
|
|
||||||
errorMessage: 'ارسال پیامک از پنل ادمین غیرفعال شده است.',
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.username || !config.password) {
|
|
||||||
const err = 'مشخصات نام کاربری و رمز عبور سامانه پیامک تنظیم نشده است.';
|
|
||||||
this.logger.error(`[SMS MISCONFIGURED] ${err}`);
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: err,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
username: config.username,
|
username: this.username,
|
||||||
password: config.password,
|
password: this.password,
|
||||||
text: options.args.join(';'),
|
text: options.args.join(';'),
|
||||||
to: options.to,
|
to: options.to,
|
||||||
bodyId: options.bodyId,
|
bodyId: options.bodyId,
|
||||||
@ -657,9 +51,8 @@ export class SmsService {
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
res.on('data', (chunk) => (data += chunk));
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
void (async () => {
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||||
const val = json.Value ?? 0;
|
const val = json.Value ?? 0;
|
||||||
@ -667,63 +60,25 @@ export class SmsService {
|
|||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
|
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
|
||||||
);
|
);
|
||||||
await this.recordLog({
|
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'SUCCESS',
|
|
||||||
recId: String(val),
|
|
||||||
});
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
|
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
|
`[SMS Error] Failed sending pattern SMS to ${options.to}. Code: ${val}`,
|
||||||
);
|
);
|
||||||
await this.recordLog({
|
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'FAILED',
|
|
||||||
recId: String(val),
|
|
||||||
errorMessage: errMsg,
|
|
||||||
});
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
|
|
||||||
});
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
req.on('error', (err: Error) => {
|
req.on('error', (err) => {
|
||||||
void (async () => {
|
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
|
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
|
||||||
);
|
);
|
||||||
await this.recordLog({
|
|
||||||
receptor: options.to,
|
|
||||||
type: msgType,
|
|
||||||
patternId: options.bodyId,
|
|
||||||
args: options.args,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: `خطای شبکه: ${err.message}`,
|
|
||||||
});
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
})();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
req.write(payload);
|
req.write(payload);
|
||||||
@ -732,190 +87,73 @@ export class SmsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test SMS Dispatch from Admin Panel
|
* Send OTP Verification Code (Pattern 508079)
|
||||||
*/
|
|
||||||
async testSms(
|
|
||||||
targetPhone: string,
|
|
||||||
testPatternId?: number,
|
|
||||||
testArgs?: string[],
|
|
||||||
): Promise<{
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
rawResponse?: MeliPayamakResponse;
|
|
||||||
}> {
|
|
||||||
const config = await this.getSmsConfig();
|
|
||||||
|
|
||||||
if (!config.username || !config.password) {
|
|
||||||
const msg = 'نام کاربری یا رمز عبور سامانه ملی پیامک تنظیم نشده است.';
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: targetPhone,
|
|
||||||
type: 'TEST',
|
|
||||||
patternId: testPatternId,
|
|
||||||
args: testArgs,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: msg,
|
|
||||||
});
|
|
||||||
return { success: false, message: msg };
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyId = testPatternId || config.otpBodyId || 508079;
|
|
||||||
const args = testArgs && testArgs.length > 0 ? testArgs : ['123456'];
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const payload = JSON.stringify({
|
|
||||||
username: config.username,
|
|
||||||
password: config.password,
|
|
||||||
text: args.join(';'),
|
|
||||||
to: targetPhone,
|
|
||||||
bodyId: bodyId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = https.request(
|
|
||||||
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': Buffer.byteLength(payload),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
|
||||||
res.on('end', () => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
|
||||||
const val = json.Value ?? 0;
|
|
||||||
if (json && (val > 15 || json.RetStatus === 1)) {
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: targetPhone,
|
|
||||||
type: 'TEST',
|
|
||||||
patternId: bodyId,
|
|
||||||
args,
|
|
||||||
status: 'SUCCESS',
|
|
||||||
recId: String(val),
|
|
||||||
});
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
message: `پیامک تستی پترن با موفقیت ارسال شد (شناسه پیگیری ملی پیامک: ${val})`,
|
|
||||||
rawResponse: json,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const errMsg = `خطای درگاه ملی پیامک: کد پاسخ بازگشتی ${val}`;
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: targetPhone,
|
|
||||||
type: 'TEST',
|
|
||||||
patternId: bodyId,
|
|
||||||
args,
|
|
||||||
status: 'FAILED',
|
|
||||||
recId: String(val),
|
|
||||||
errorMessage: errMsg,
|
|
||||||
});
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
message: errMsg,
|
|
||||||
rawResponse: json,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
const errMsg = `پاسخ نامعتبر از سرور ملی پیامک: ${data || msg}`;
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: targetPhone,
|
|
||||||
type: 'TEST',
|
|
||||||
patternId: bodyId,
|
|
||||||
args,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: errMsg,
|
|
||||||
});
|
|
||||||
resolve({ success: false, message: errMsg });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
req.on('error', (err: Error) => {
|
|
||||||
void (async () => {
|
|
||||||
const errMsg = `خطای برقراری ارتباط با وبسرویس ملی پیامک: ${err.message}`;
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: targetPhone,
|
|
||||||
type: 'TEST',
|
|
||||||
patternId: bodyId,
|
|
||||||
args,
|
|
||||||
status: 'FAILED',
|
|
||||||
errorMessage: errMsg,
|
|
||||||
});
|
|
||||||
resolve({ success: false, message: errMsg });
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
req.write(payload);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send OTP Verification Code
|
|
||||||
*/
|
*/
|
||||||
async sendOtp(phone: string, otpCode: string): Promise<boolean> {
|
async sendOtp(phone: string, otpCode: string): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
const bodyId = parseInt(
|
||||||
|
process.env.MELIPAYAMAK_OTP_BODY_ID || '508079',
|
||||||
|
10,
|
||||||
|
);
|
||||||
return this.sendPatternSms({
|
return this.sendPatternSms({
|
||||||
to: phone,
|
to: phone,
|
||||||
bodyId: config.otpBodyId,
|
bodyId,
|
||||||
args: [otpCode],
|
args: [otpCode],
|
||||||
type: 'OTP',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Order Confirmation SMS
|
* Send Order Confirmation SMS (Pattern 508081)
|
||||||
*/
|
*/
|
||||||
async sendOrderConfirmation(
|
async sendOrderConfirmation(
|
||||||
phone: string,
|
phone: string,
|
||||||
orderNumber: string,
|
orderNumber: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
const bodyId = parseInt(
|
||||||
|
process.env.MELIPAYAMAK_ORDER_BODY_ID || '508081',
|
||||||
|
10,
|
||||||
|
);
|
||||||
return this.sendPatternSms({
|
return this.sendPatternSms({
|
||||||
to: phone,
|
to: phone,
|
||||||
bodyId: config.orderBodyId,
|
bodyId,
|
||||||
args: [orderNumber, amount],
|
args: [orderNumber, amount],
|
||||||
type: 'ORDER_CONFIRMATION',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Shipping Status SMS with Tracking Code
|
* Send Shipping Status SMS with Tracking Code (Pattern 508082)
|
||||||
*/
|
*/
|
||||||
async sendShippingNotification(
|
async sendShippingNotification(
|
||||||
phone: string,
|
phone: string,
|
||||||
orderNumber: string,
|
orderNumber: string,
|
||||||
trackingCode: string,
|
trackingCode: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
const bodyId = parseInt(
|
||||||
|
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '508082',
|
||||||
|
10,
|
||||||
|
);
|
||||||
return this.sendPatternSms({
|
return this.sendPatternSms({
|
||||||
to: phone,
|
to: phone,
|
||||||
bodyId: config.shippingBodyId,
|
bodyId,
|
||||||
args: [orderNumber, trackingCode],
|
args: [orderNumber, trackingCode],
|
||||||
type: 'SHIPPING_TRACKING',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send B2B Application Notification SMS
|
* Send B2B Application Notification SMS (Pattern 508083)
|
||||||
*/
|
*/
|
||||||
async sendB2bNotification(
|
async sendB2bNotification(
|
||||||
phone: string,
|
phone: string,
|
||||||
applicantName: string,
|
applicantName: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
const bodyId = parseInt(
|
||||||
|
process.env.MELIPAYAMAK_B2B_BODY_ID || '508083',
|
||||||
|
10,
|
||||||
|
);
|
||||||
return this.sendPatternSms({
|
return this.sendPatternSms({
|
||||||
to: phone,
|
to: phone,
|
||||||
bodyId: config.b2bBodyId,
|
bodyId,
|
||||||
args: [applicantName],
|
args: [applicantName],
|
||||||
type: 'B2B_NOTIFICATION',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -927,13 +165,14 @@ export class SmsService {
|
|||||||
petName: string,
|
petName: string,
|
||||||
reminderType: string,
|
reminderType: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
const bodyId = parseInt(
|
||||||
if (!config.petCareBodyId) return false;
|
process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0',
|
||||||
|
10,
|
||||||
|
);
|
||||||
return this.sendPatternSms({
|
return this.sendPatternSms({
|
||||||
to: phone,
|
to: phone,
|
||||||
bodyId: config.petCareBodyId,
|
bodyId,
|
||||||
args: [petName, reminderType],
|
args: [petName, reminderType],
|
||||||
type: 'PET_CARE_REMINDER',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -941,24 +180,7 @@ export class SmsService {
|
|||||||
* Generic text SMS dispatch
|
* Generic text SMS dispatch
|
||||||
*/
|
*/
|
||||||
async sendSms(phone: string, message: string): Promise<boolean> {
|
async sendSms(phone: string, message: string): Promise<boolean> {
|
||||||
const config = await this.getSmsConfig();
|
|
||||||
if (!config.enabled) {
|
|
||||||
await this.recordLog({
|
|
||||||
receptor: phone,
|
|
||||||
type: 'GENERIC_TEXT',
|
|
||||||
messageText: message,
|
|
||||||
status: 'DISABLED',
|
|
||||||
errorMessage: 'ارسال پیامک غیرفعال است.',
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.logger.log(`[SMS Text Sent] To: ${phone}, Content: ${message}`);
|
this.logger.log(`[SMS Text Sent] To: ${phone}, Content: ${message}`);
|
||||||
await this.recordLog({
|
|
||||||
receptor: phone,
|
|
||||||
type: 'GENERIC_TEXT',
|
|
||||||
messageText: message,
|
|
||||||
status: 'SUCCESS',
|
|
||||||
});
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
InitiateWalletTopupDto,
|
||||||
} from './dto/initiate-payment.dto';
|
} from './dto/initiate-payment.dto';
|
||||||
import { ZibalCallbackQueryDto } from './dto/verify-payment.dto';
|
import { ZibalCallbackQueryDto } from './dto/verify-payment.dto';
|
||||||
import { AdminTransactionFilterDto } from './dto/admin-transaction-filter.dto';
|
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { RolesGuard } from '../common/guards/roles.guard';
|
|
||||||
import { Roles } from '../common/decorators/roles.decorator';
|
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiBadRequestResponse,
|
ApiBadRequestResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
@ApiTags('Payment - درگاه پرداخت اینترنتی زیبال و مدیریت تراکنشها')
|
@ApiTags('Payment - درگاه پرداخت اینترنتی زیبال (Zibal IPG)')
|
||||||
@Controller('payment')
|
@Controller('payment')
|
||||||
export class PaymentController {
|
export class PaymentController {
|
||||||
constructor(
|
constructor(
|
||||||
@ -54,9 +52,7 @@ export class PaymentController {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ApiBadRequestResponse({
|
@ApiBadRequestResponse({ description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش' })
|
||||||
description: 'خطا در ایجاد تراکنش یا نامعتبر بودن سفارش',
|
|
||||||
})
|
|
||||||
async initiateOrderPayment(
|
async initiateOrderPayment(
|
||||||
@Req() req: { user: { id: string } },
|
@Req() req: { user: { id: string } },
|
||||||
@Body() body: InitiatePaymentDto,
|
@Body() body: InitiatePaymentDto,
|
||||||
@ -74,6 +70,14 @@ export class PaymentController {
|
|||||||
@ApiOperation({ summary: 'شارژ آنلاین کیف پول از طریق درگاه زیبال' })
|
@ApiOperation({ summary: 'شارژ آنلاین کیف پول از طریق درگاه زیبال' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: 'لینک هدایت به درگاه پرداخت برای شارژ کیف پول',
|
description: 'لینک هدایت به درگاه پرداخت برای شارژ کیف پول',
|
||||||
|
schema: {
|
||||||
|
example: {
|
||||||
|
success: true,
|
||||||
|
trackId: '15966442233311',
|
||||||
|
paymentUrl: 'https://gateway.zibal.ir/start/15966442233311',
|
||||||
|
amount: 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
async initiateWalletTopup(
|
async initiateWalletTopup(
|
||||||
@Req() req: { user: { id: string } },
|
@Req() req: { user: { id: string } },
|
||||||
@ -87,9 +91,7 @@ export class PaymentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('zibal/callback')
|
@Get('zibal/callback')
|
||||||
@ApiOperation({
|
@ApiOperation({ summary: 'دریافت کالبک بازگشت از درگاه زیبال و تایید تراکنش' })
|
||||||
summary: 'دریافت کالبک بازگشت از درگاه زیبال و تایید تراکنش',
|
|
||||||
})
|
|
||||||
async handleZibalCallback(
|
async handleZibalCallback(
|
||||||
@Query() query: ZibalCallbackQueryDto,
|
@Query() query: ZibalCallbackQueryDto,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@ -128,10 +130,9 @@ export class PaymentController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.redirect(`${frontendUrl}/payment/verify?${params.toString()}`);
|
return res.redirect(`${frontendUrl}/payment/verify?${params.toString()}`);
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
const msg = err instanceof Error ? err.message : 'خطا در پردازش پرداخت';
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
`${frontendUrl}/payment/verify?success=0&trackId=${trackId}&message=${encodeURIComponent(msg)}`,
|
`${frontendUrl}/payment/verify?success=0&trackId=${trackId}&message=${encodeURIComponent(err.message || 'خطا در پردازش پرداخت')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,9 +149,7 @@ export class PaymentController {
|
|||||||
const orderId = body.orderId;
|
const orderId = body.orderId;
|
||||||
|
|
||||||
if (!trackId) {
|
if (!trackId) {
|
||||||
return res
|
return res.status(HttpStatus.BAD_REQUEST).json({ success: false, message: 'trackId is required' });
|
||||||
.status(HttpStatus.BAD_REQUEST)
|
|
||||||
.json({ success: false, message: 'trackId is required' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.paymentService.verifyAndProcess(
|
const result = await this.paymentService.verifyAndProcess(
|
||||||
@ -168,42 +167,4 @@ export class PaymentController {
|
|||||||
async getTransactionStatus(@Param('trackId') trackId: string) {
|
async getTransactionStatus(@Param('trackId') trackId: string) {
|
||||||
return this.paymentService.getTransaction(trackId);
|
return this.paymentService.getTransaction(trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ADMIN ENDPOINTS FOR TRANSACTION LOGS & DIAGNOSTICS ---
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('admin/transactions')
|
|
||||||
@ApiOperation({ summary: 'مدیریت و مشاهده تمامی لاگها و تراکنشهای پرداخت' })
|
|
||||||
async getAdminTransactions(@Query() query: AdminTransactionFilterDto) {
|
|
||||||
return this.paymentService.getAdminTransactions(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('admin/stats')
|
|
||||||
@ApiOperation({ summary: 'آمار کلی تراکنشها و حجم مبالغ پرداختی' })
|
|
||||||
async getAdminStats() {
|
|
||||||
return this.paymentService.getAdminTransactionStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Post('admin/reconcile')
|
|
||||||
@ApiOperation({ summary: 'اجرای فرایند انطباق و استعلام خودکار تراکنشهای بلاتکلیف' })
|
|
||||||
async reconcilePending() {
|
|
||||||
return this.paymentService.reconcilePendingTransactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('admin/live-inquiry/:id')
|
|
||||||
@ApiOperation({ summary: 'استعلام زنده وضعیت تراکنش از سرور زیبال' })
|
|
||||||
async liveInquiry(@Param('id') id: string) {
|
|
||||||
return this.paymentService.adminLiveInquiry(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
import { ZibalService } from './zibal.service';
|
import { ZibalService } from './zibal.service';
|
||||||
import { SmsService } from '../common/services/sms.service';
|
import { SmsService } from '../common/services/sms.service';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { AdminTransactionFilterDto } from './dto/admin-transaction-filter.dto';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentService {
|
export class PaymentService {
|
||||||
@ -37,20 +36,21 @@ export class PaymentService {
|
|||||||
throw new NotFoundException('سفارش مورد نظر یافت نشد');
|
throw new NotFoundException('سفارش مورد نظر یافت نشد');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (order.status !== 'processing' && order.status !== 'pending_payment') {
|
||||||
if (['shipped', 'delivered'].includes(order.status)) {
|
if (['shipped', 'delivered'].includes(order.status)) {
|
||||||
throw new BadRequestException('این سفارش قبلاً تکمیل و ارسال شده است');
|
throw new BadRequestException('این سفارش قبلاً تکمیل و ارسال شده است');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const amountTomans = Number(order.totalAmount);
|
const amountTomans = Number(order.totalAmount);
|
||||||
const amountRials = Math.round(amountTomans * 10);
|
const amountRials = Math.round(amountTomans * 10);
|
||||||
|
|
||||||
if (amountRials < 1000) {
|
if (amountRials < 1000) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException('مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است');
|
||||||
'مبلغ سفارش کمتر از حداقل مجاز درگاه بانکی (۱,۰۰۰ ریال) است',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const frontendUrl = await this.zibalService.getFrontendUrl();
|
const frontendUrl = await this.zibalService.getFrontendUrl();
|
||||||
|
// Default callback points to frontend verify page (or backend proxy)
|
||||||
const callbackUrl =
|
const callbackUrl =
|
||||||
customCallbackUrl || `${frontendUrl}/payment/verify?orderId=${order.id}`;
|
customCallbackUrl || `${frontendUrl}/payment/verify?orderId=${order.id}`;
|
||||||
|
|
||||||
@ -111,11 +111,9 @@ export class PaymentService {
|
|||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
amount: amountTomans,
|
amount: amountTomans,
|
||||||
};
|
};
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
this.logger.error(`Error requesting payment from Zibal: ${err.message}`, err.stack);
|
||||||
const stack = err instanceof Error ? err.stack : undefined;
|
throw new BadRequestException(err.message || 'خطا در اتصال به درگاه پرداخت');
|
||||||
this.logger.error(`Error requesting payment from Zibal: ${msg}`, stack);
|
|
||||||
throw new BadRequestException(msg || 'خطا در اتصال به درگاه پرداخت');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,15 +189,14 @@ export class PaymentService {
|
|||||||
paymentUrl: this.zibalService.getStartUrl(trackId),
|
paymentUrl: this.zibalService.getStartUrl(trackId),
|
||||||
amount: amountTomans,
|
amount: amountTomans,
|
||||||
};
|
};
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
this.logger.error(`Error requesting wallet topup from Zibal: ${err.message}`);
|
||||||
this.logger.error(`Error requesting wallet topup from Zibal: ${msg}`);
|
throw new BadRequestException(err.message || 'خطا در ارتباط با درگاه بانکی');
|
||||||
throw new BadRequestException(msg || 'خطا در ارتباط با درگاه بانکی');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3. Verify & Process Callback from Zibal (With Idempotency & Amount Match Check)
|
* 3. Verify & Process Callback from Zibal
|
||||||
*/
|
*/
|
||||||
async verifyAndProcess(
|
async verifyAndProcess(
|
||||||
trackId: string,
|
trackId: string,
|
||||||
@ -215,42 +212,34 @@ export class PaymentService {
|
|||||||
where: { trackId },
|
where: { trackId },
|
||||||
include: {
|
include: {
|
||||||
order: {
|
order: {
|
||||||
include: {
|
include: { user: true, orderItems: { include: { product: true } } },
|
||||||
user: true,
|
|
||||||
orderItems: { include: { product: true } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!transaction && orderIdParam) {
|
if (!transaction) {
|
||||||
|
// If transaction not found by trackId, check if orderId exists
|
||||||
|
if (orderIdParam) {
|
||||||
transaction = await this.prisma.paymentTransaction.findFirst({
|
transaction = await this.prisma.paymentTransaction.findFirst({
|
||||||
where: { orderId: orderIdParam },
|
where: { orderId: orderIdParam },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: {
|
include: {
|
||||||
order: {
|
order: {
|
||||||
include: {
|
include: { user: true, orderItems: { include: { product: true } } },
|
||||||
user: true,
|
|
||||||
orderItems: { include: { product: true } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
throw new NotFoundException('تراکنش پرداخت در سیستم یافت نشد');
|
throw new NotFoundException('تراکنش پرداخت در سیستم یافت نشد');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- IDEMPOTENCY CHECK ---
|
// If transaction is already verified, return successful details immediately
|
||||||
// If transaction is already marked as VERIFIED, return stored confirmation immediately
|
|
||||||
// without executing any duplicate side-effects (wallet increment, charity, status change, SMS).
|
|
||||||
if (transaction.status === 'VERIFIED') {
|
if (transaction.status === 'VERIFIED') {
|
||||||
this.logger.log(
|
|
||||||
`Idempotency Guard: Transaction ${transaction.id} (trackId=${trackId}) is already VERIFIED. Skipping duplicate execution.`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
alreadyVerified: true,
|
alreadyVerified: true,
|
||||||
@ -260,16 +249,13 @@ export class PaymentService {
|
|||||||
cardNumber: transaction.cardNumber,
|
cardNumber: transaction.cardNumber,
|
||||||
amount: Number(transaction.amount),
|
amount: Number(transaction.amount),
|
||||||
orderId: transaction.orderId,
|
orderId: transaction.orderId,
|
||||||
type: transaction.type,
|
|
||||||
message: 'تراکنش قبلاً با موفقیت تایید و پردازش شده است',
|
message: 'تراکنش قبلاً با موفقیت تایید و پردازش شده است',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user cancelled or Zibal signaled failure at the gate
|
// If user cancelled or Zibal signaled failure at the gate
|
||||||
if (successParam === '0' || statusParam === '3') {
|
if (successParam === '0' || statusParam === '3') {
|
||||||
const statusMessage = this.zibalService.getStatusMessage(
|
const statusMessage = this.zibalService.getStatusMessage(statusParam || 3);
|
||||||
statusParam || 3,
|
|
||||||
);
|
|
||||||
await this.prisma.paymentTransaction.update({
|
await this.prisma.paymentTransaction.update({
|
||||||
where: { id: transaction.id },
|
where: { id: transaction.id },
|
||||||
data: {
|
data: {
|
||||||
@ -314,53 +300,15 @@ export class PaymentService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AMOUNT MATCH VERIFICATION ---
|
|
||||||
const expectedAmountRials = Number(
|
|
||||||
transaction.amountRials ||
|
|
||||||
Math.round(Number(transaction.amount) * 10),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
verifyRes.amount &&
|
|
||||||
Number(verifyRes.amount) > 0 &&
|
|
||||||
Number(verifyRes.amount) !== expectedAmountRials
|
|
||||||
) {
|
|
||||||
this.logger.error(
|
|
||||||
`Security Alert: Amount mismatch on trackId=${trackId}! Zibal reported ${verifyRes.amount} Rials, but expected ${expectedAmountRials} Rials in DB.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.prisma.paymentTransaction.update({
|
|
||||||
where: { id: transaction.id },
|
|
||||||
data: {
|
|
||||||
status: 'FAILED',
|
|
||||||
resultCode: verifyRes.result,
|
|
||||||
message: `مغایرت مالی: مبلغ تایید شده زیبال (${verifyRes.amount} ریال) با مبلغ سیستم (${expectedAmountRials} ریال) مطابقت ندارد.`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: 'FAILED',
|
|
||||||
trackId,
|
|
||||||
message: 'خطای امنیتی: مغایرت در مبلغ پرداخت شده با سفارش.',
|
|
||||||
orderId: transaction.orderId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// SUCCESSFUL PAYMENT!
|
// SUCCESSFUL PAYMENT!
|
||||||
const refNumber = verifyRes.refNumber
|
const refNumber = verifyRes.refNumber ? String(verifyRes.refNumber) : undefined;
|
||||||
? String(verifyRes.refNumber)
|
|
||||||
: undefined;
|
|
||||||
const cardNumber = verifyRes.cardNumber || undefined;
|
const cardNumber = verifyRes.cardNumber || undefined;
|
||||||
const paidDate = verifyRes.paidAt
|
const paidDate = verifyRes.paidAt ? new Date(verifyRes.paidAt) : new Date();
|
||||||
? new Date(verifyRes.paidAt)
|
|
||||||
: new Date();
|
|
||||||
|
|
||||||
// Execute atomic status update with concurrency guard
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
// Optimistic lock: only update if still PENDING (prevents race condition)
|
// 1. Mark transaction as VERIFIED
|
||||||
const updatedCount = await tx.paymentTransaction.updateMany({
|
await tx.paymentTransaction.update({
|
||||||
where: { id: transaction.id, status: 'PENDING' },
|
where: { id: transaction.id },
|
||||||
data: {
|
data: {
|
||||||
status: 'VERIFIED',
|
status: 'VERIFIED',
|
||||||
resultCode: verifyRes.result,
|
resultCode: verifyRes.result,
|
||||||
@ -371,14 +319,6 @@ export class PaymentService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// If another concurrent thread verified it first, exit safely
|
|
||||||
if (updatedCount.count === 0) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Concurrent execution detected for transaction ${transaction.id}. Handled safely.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. If it's an Order payment
|
// 2. If it's an Order payment
|
||||||
if (transaction.type === 'ORDER' && transaction.orderId) {
|
if (transaction.type === 'ORDER' && transaction.orderId) {
|
||||||
await tx.order.update({
|
await tx.order.update({
|
||||||
@ -390,10 +330,7 @@ export class PaymentService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// If charity donation was made, update user's total charity
|
// If charity donation was made, update user's total charity
|
||||||
if (
|
if (transaction.order && Number(transaction.order.charityDonation) > 0) {
|
||||||
transaction.order &&
|
|
||||||
Number(transaction.order.charityDonation) > 0
|
|
||||||
) {
|
|
||||||
await tx.user.update({
|
await tx.user.update({
|
||||||
where: { id: transaction.userId },
|
where: { id: transaction.userId },
|
||||||
data: {
|
data: {
|
||||||
@ -432,11 +369,8 @@ export class PaymentService {
|
|||||||
// Send SMS notification if order payment
|
// Send SMS notification if order payment
|
||||||
if (transaction.type === 'ORDER' && transaction.order?.user?.mobile) {
|
if (transaction.type === 'ORDER' && transaction.order?.user?.mobile) {
|
||||||
const mobile = transaction.order.user.mobile;
|
const mobile = transaction.order.user.mobile;
|
||||||
const trackingNum =
|
const trackingNum = transaction.order.trackingNumber || transaction.order.id;
|
||||||
transaction.order.trackingNumber || transaction.order.id;
|
const formattedAmount = Number(transaction.amount).toLocaleString('fa-IR');
|
||||||
const formattedAmount = Number(transaction.amount).toLocaleString(
|
|
||||||
'fa-IR',
|
|
||||||
);
|
|
||||||
|
|
||||||
this.smsService
|
this.smsService
|
||||||
.sendOrderConfirmation(mobile, trackingNum, formattedAmount)
|
.sendOrderConfirmation(mobile, trackingNum, formattedAmount)
|
||||||
@ -456,92 +390,20 @@ export class PaymentService {
|
|||||||
type: transaction.type,
|
type: transaction.type,
|
||||||
message: 'پرداخت با موفقیت انجام شد',
|
message: 'پرداخت با موفقیت انجام شد',
|
||||||
};
|
};
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
this.logger.error(`Error during verify: ${err.message}`, err.stack);
|
||||||
const stack = err instanceof Error ? err.stack : undefined;
|
|
||||||
this.logger.error(`Error during verify: ${msg}`, stack);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: 'ERROR',
|
status: 'ERROR',
|
||||||
trackId,
|
trackId,
|
||||||
message: msg || 'خطا در فرایند تایید تراکنش',
|
message: err.message || 'خطا در فرایند تایید تراکنش',
|
||||||
orderId: transaction.orderId,
|
orderId: transaction.orderId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 4. Auto-Reconciliation / Cron Job for Pending Transactions (استعلام تراکنشهای بلاتکلیف)
|
* 4. Get Payment Transaction Details
|
||||||
*/
|
|
||||||
async reconcilePendingTransactions() {
|
|
||||||
this.logger.log('Starting pending transactions reconciliation job...');
|
|
||||||
|
|
||||||
// Find transactions that are PENDING and created between 5 minutes and 24 hours ago
|
|
||||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
||||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const pendingTransactions = await this.prisma.paymentTransaction.findMany({
|
|
||||||
where: {
|
|
||||||
status: 'PENDING',
|
|
||||||
trackId: { not: null },
|
|
||||||
createdAt: {
|
|
||||||
gte: oneDayAgo,
|
|
||||||
lte: fiveMinutesAgo,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: { order: true, user: true },
|
|
||||||
take: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
total: pendingTransactions.length,
|
|
||||||
verified: 0,
|
|
||||||
failed: 0,
|
|
||||||
remainedPending: 0,
|
|
||||||
errors: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const tx of pendingTransactions) {
|
|
||||||
if (!tx.trackId) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inquiry = await this.zibalService.inquiryPayment(tx.trackId);
|
|
||||||
this.logger.log(
|
|
||||||
`Inquiry for trackId=${tx.trackId}: status=${inquiry.status}, result=${inquiry.result}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Status 1 (paid & verified) or 2 (paid, unverified)
|
|
||||||
if (inquiry.status === 1 || inquiry.status === 2 || inquiry.result === 100) {
|
|
||||||
await this.verifyAndProcess(tx.trackId, '1', String(inquiry.status), tx.orderId || undefined);
|
|
||||||
results.verified++;
|
|
||||||
} else if (inquiry.status === 3 || inquiry.status === -2 || inquiry.result === 202) {
|
|
||||||
// Cancelled or failed
|
|
||||||
const msg = this.zibalService.getStatusMessage(inquiry.status || 3);
|
|
||||||
await this.prisma.paymentTransaction.update({
|
|
||||||
where: { id: tx.id },
|
|
||||||
data: {
|
|
||||||
status: 'FAILED',
|
|
||||||
resultCode: inquiry.result,
|
|
||||||
message: msg,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
results.failed++;
|
|
||||||
} else {
|
|
||||||
results.remainedPending++;
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
results.errors++;
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
this.logger.warn(`Failed inquiry for trackId=${tx.trackId}: ${msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Reconciliation finished: ${JSON.stringify(results)}`);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 5. Get Payment Transaction Details by TrackID
|
|
||||||
*/
|
*/
|
||||||
async getTransaction(trackId: string) {
|
async getTransaction(trackId: string) {
|
||||||
const transaction = await this.prisma.paymentTransaction.findUnique({
|
const transaction = await this.prisma.paymentTransaction.findUnique({
|
||||||
@ -554,7 +416,6 @@ export class PaymentService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -575,215 +436,6 @@ export class PaymentService {
|
|||||||
paidAt: transaction.paidAt,
|
paidAt: transaction.paidAt,
|
||||||
createdAt: transaction.createdAt,
|
createdAt: transaction.createdAt,
|
||||||
order: transaction.order,
|
order: transaction.order,
|
||||||
user: transaction.user ? {
|
|
||||||
id: transaction.user.id,
|
|
||||||
firstName: transaction.user.firstName,
|
|
||||||
lastName: transaction.user.lastName,
|
|
||||||
mobile: transaction.user.mobile,
|
|
||||||
} : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 6. Admin: Get List of Transactions with Advanced Search & Filter
|
|
||||||
*/
|
|
||||||
async getAdminTransactions(filters: AdminTransactionFilterDto) {
|
|
||||||
const {
|
|
||||||
page = 1,
|
|
||||||
limit = 15,
|
|
||||||
search,
|
|
||||||
status,
|
|
||||||
gateway,
|
|
||||||
type,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
minAmount,
|
|
||||||
maxAmount,
|
|
||||||
sortBy = 'createdAt',
|
|
||||||
sortOrder = 'desc',
|
|
||||||
} = filters;
|
|
||||||
|
|
||||||
const skip = (page - 1) * limit;
|
|
||||||
const where: Prisma.PaymentTransactionWhereInput = {};
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
where.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gateway) {
|
|
||||||
where.gateway = gateway;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type) {
|
|
||||||
where.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startDate || endDate) {
|
|
||||||
where.createdAt = {};
|
|
||||||
if (startDate) where.createdAt.gte = new Date(startDate);
|
|
||||||
if (endDate) where.createdAt.lte = new Date(endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minAmount !== undefined || maxAmount !== undefined) {
|
|
||||||
where.amount = {};
|
|
||||||
if (minAmount !== undefined) where.amount.gte = new Prisma.Decimal(minAmount);
|
|
||||||
if (maxAmount !== undefined) where.amount.lte = new Prisma.Decimal(maxAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search && search.trim() !== '') {
|
|
||||||
const s = search.trim();
|
|
||||||
where.OR = [
|
|
||||||
{ trackId: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ refNumber: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ cardNumber: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ description: { contains: s, mode: 'insensitive' } },
|
|
||||||
{
|
|
||||||
user: {
|
|
||||||
OR: [
|
|
||||||
{ firstName: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ lastName: { contains: s, mode: 'insensitive' } },
|
|
||||||
{ mobile: { contains: s, mode: 'insensitive' } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: {
|
|
||||||
trackingNumber: { contains: s, mode: 'insensitive' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
|
||||||
this.prisma.paymentTransaction.findMany({
|
|
||||||
where,
|
|
||||||
skip,
|
|
||||||
take: limit,
|
|
||||||
orderBy: { [sortBy]: sortOrder },
|
|
||||||
include: {
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
firstName: true,
|
|
||||||
lastName: true,
|
|
||||||
mobile: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
trackingNumber: true,
|
|
||||||
totalAmount: true,
|
|
||||||
status: true,
|
|
||||||
paymentMethod: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.prisma.paymentTransaction.count({ where }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
lastPage: Math.ceil(total / limit),
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 7. Admin: Get Transaction Statistics
|
|
||||||
*/
|
|
||||||
async getAdminTransactionStats() {
|
|
||||||
const [
|
|
||||||
totalCount,
|
|
||||||
verifiedCount,
|
|
||||||
pendingCount,
|
|
||||||
failedCount,
|
|
||||||
verifiedSum,
|
|
||||||
todayVerified,
|
|
||||||
] = await Promise.all([
|
|
||||||
this.prisma.paymentTransaction.count(),
|
|
||||||
this.prisma.paymentTransaction.count({ where: { status: 'VERIFIED' } }),
|
|
||||||
this.prisma.paymentTransaction.count({ where: { status: 'PENDING' } }),
|
|
||||||
this.prisma.paymentTransaction.count({ where: { status: 'FAILED' } }),
|
|
||||||
this.prisma.paymentTransaction.aggregate({
|
|
||||||
_sum: { amount: true },
|
|
||||||
where: { status: 'VERIFIED' },
|
|
||||||
}),
|
|
||||||
this.prisma.paymentTransaction.aggregate({
|
|
||||||
_sum: { amount: true },
|
|
||||||
where: {
|
|
||||||
status: 'VERIFIED',
|
|
||||||
createdAt: {
|
|
||||||
gte: new Date(new Date().setHours(0, 0, 0, 0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const totalVolume = Number(verifiedSum._sum.amount || 0);
|
|
||||||
const todayVolume = Number(todayVerified._sum.amount || 0);
|
|
||||||
const successRate =
|
|
||||||
totalCount > 0 ? Math.round((verifiedCount / totalCount) * 100) : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalCount,
|
|
||||||
verifiedCount,
|
|
||||||
pendingCount,
|
|
||||||
failedCount,
|
|
||||||
totalVolume,
|
|
||||||
todayVolume,
|
|
||||||
successRate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 8. Admin: Perform Live Instant Inquiry on Zibal for a Transaction
|
|
||||||
*/
|
|
||||||
async adminLiveInquiry(transactionId: string) {
|
|
||||||
const transaction = await this.prisma.paymentTransaction.findUnique({
|
|
||||||
where: { id: transactionId },
|
|
||||||
include: { order: true, user: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!transaction) {
|
|
||||||
throw new NotFoundException('تراکنش یافت نشد');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transaction.trackId) {
|
|
||||||
throw new BadRequestException('این تراکنش فاقد TrackId زیبال است');
|
|
||||||
}
|
|
||||||
|
|
||||||
const inquiryRes = await this.zibalService.inquiryPayment(transaction.trackId);
|
|
||||||
const statusMsg = this.zibalService.getStatusMessage(inquiryRes.status);
|
|
||||||
const resultMsg = this.zibalService.getResultMessage(inquiryRes.result);
|
|
||||||
|
|
||||||
// If transaction was PENDING and inquiry confirms payment, sync automatically
|
|
||||||
if (
|
|
||||||
transaction.status === 'PENDING' &&
|
|
||||||
(inquiryRes.status === 1 || inquiryRes.status === 2 || inquiryRes.result === 100)
|
|
||||||
) {
|
|
||||||
await this.verifyAndProcess(
|
|
||||||
transaction.trackId,
|
|
||||||
'1',
|
|
||||||
String(inquiryRes.status),
|
|
||||||
transaction.orderId || undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
transactionId: transaction.id,
|
|
||||||
trackId: transaction.trackId,
|
|
||||||
currentDbStatus: transaction.status,
|
|
||||||
gatewayResponse: inquiryRes,
|
|
||||||
statusMessage: statusMsg,
|
|
||||||
resultMessage: resultMsg,
|
|
||||||
inquiredAt: new Date(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,12 +115,8 @@ export class ZibalService {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
this.logger.error(
|
this.logger.error(`Zibal request HTTP error ${response.status}: ${errorText}`);
|
||||||
`Zibal request HTTP error ${response.status}: ${errorText}`,
|
throw new Error(`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`);
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as ZibalRequestResponse;
|
const data = (await response.json()) as ZibalRequestResponse;
|
||||||
@ -146,9 +142,7 @@ export class ZibalService {
|
|||||||
trackId: String(trackId),
|
trackId: String(trackId),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`);
|
||||||
`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/v1/verify`, {
|
const response = await fetch(`${this.baseUrl}/v1/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -160,12 +154,8 @@ export class ZibalService {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
this.logger.error(
|
this.logger.error(`Zibal verify HTTP error ${response.status}: ${errorText}`);
|
||||||
`Zibal verify HTTP error ${response.status}: ${errorText}`,
|
throw new Error(`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`);
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as ZibalVerifyResponse;
|
const data = (await response.json()) as ZibalVerifyResponse;
|
||||||
@ -176,9 +166,7 @@ export class ZibalService {
|
|||||||
/**
|
/**
|
||||||
* 4. Inquiry Payment (استعلام وضعیت تراکنش)
|
* 4. Inquiry Payment (استعلام وضعیت تراکنش)
|
||||||
*/
|
*/
|
||||||
async inquiryPayment(
|
async inquiryPayment(trackId: string | number): Promise<ZibalInquiryResponse> {
|
||||||
trackId: string | number,
|
|
||||||
): Promise<ZibalInquiryResponse> {
|
|
||||||
const merchant = await this.getMerchant();
|
const merchant = await this.getMerchant();
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -198,12 +186,8 @@ export class ZibalService {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
this.logger.error(
|
this.logger.error(`Zibal inquiry HTTP error ${response.status}: ${errorText}`);
|
||||||
`Zibal inquiry HTTP error ${response.status}: ${errorText}`,
|
throw new Error(`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`);
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as ZibalInquiryResponse;
|
const data = (await response.json()) as ZibalInquiryResponse;
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
|
||||||
Patch,
|
Patch,
|
||||||
Put,
|
Put,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { SettingsService, ScientificTermData } from './settings.service';
|
import { SettingsService, ScientificTermData } from './settings.service';
|
||||||
import { SmsLogQuery } from '../common/services/sms.service';
|
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { RolesGuard } from '../common/guards/roles.guard';
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
import { Roles } from '../common/decorators/roles.decorator';
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
@ -23,7 +20,7 @@ import {
|
|||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی، سیستم و پیامک')
|
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی و سیستم')
|
||||||
@Controller('settings')
|
@Controller('settings')
|
||||||
export class SettingsController {
|
export class SettingsController {
|
||||||
constructor(private readonly settingsService: SettingsService) {}
|
constructor(private readonly settingsService: SettingsService) {}
|
||||||
@ -128,101 +125,4 @@ export class SettingsController {
|
|||||||
updateSystemSettings(@Body() body: Prisma.InputJsonValue) {
|
updateSystemSettings(@Body() body: Prisma.InputJsonValue) {
|
||||||
return this.settingsService.updateCategorySetting('system', body);
|
return this.settingsService.updateCategorySetting('system', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMS Gateway Settings
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('sms')
|
|
||||||
@ApiOperation({ summary: 'دریافت تنظیمات درگاه پیامک و کدهای پترن' })
|
|
||||||
getSmsSettings() {
|
|
||||||
return this.settingsService.getSmsSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Patch('sms')
|
|
||||||
@ApiOperation({ summary: 'بهروزرسانی تنظیمات درگاه پیامک و کدهای پترن' })
|
|
||||||
updateSmsSettings(@Body() body: Prisma.InputJsonValue) {
|
|
||||||
return this.settingsService.updateSmsSettings(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Post('sms/test')
|
|
||||||
@ApiOperation({ summary: 'ارسال پیامک آزمایشی پترن به شماره دلخواه' })
|
|
||||||
testSms(
|
|
||||||
@Body('phone') phone: string,
|
|
||||||
@Body('patternId') patternId?: number,
|
|
||||||
@Body('args') args?: string[],
|
|
||||||
) {
|
|
||||||
return this.settingsService.testSms(phone, patternId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('sms/patterns')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'دریافت لیست تمامی پترنهای تعریف شده در ملی پیامک',
|
|
||||||
})
|
|
||||||
getPatterns() {
|
|
||||||
return this.settingsService.getPatterns();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Post('sms/patterns')
|
|
||||||
@ApiOperation({ summary: 'درج الگوی جدید در سامانه ملی پیامک' })
|
|
||||||
addPattern(
|
|
||||||
@Body('title') title: string,
|
|
||||||
@Body('body') body: string,
|
|
||||||
@Body('blackListId') blackListId?: number,
|
|
||||||
) {
|
|
||||||
return this.settingsService.addPattern(title, body, blackListId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Put('sms/patterns/:bodyId')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'ویرایش الگوی رد شده یا در حال ویرایش در ملی پیامک',
|
|
||||||
})
|
|
||||||
editPattern(@Param('bodyId') bodyId: string, @Body('body') body: string) {
|
|
||||||
return this.settingsService.editPattern(Number(bodyId), body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SMS Logs & Tracking Endpoints
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('sms/logs')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'دریافت گزارشات و لاگهای کامل پیامکهای ارسالی با فیلتر و جستجو',
|
|
||||||
})
|
|
||||||
getSmsLogs(@Query() query: SmsLogQuery) {
|
|
||||||
return this.settingsService.getSmsLogs(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Delete('sms/logs/:id')
|
|
||||||
@ApiOperation({ summary: 'حذف یک رکورد لاگ پیامک' })
|
|
||||||
deleteSmsLog(@Param('id') id: string) {
|
|
||||||
return this.settingsService.deleteSmsLog(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles('Admin')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Delete('sms/logs')
|
|
||||||
@ApiOperation({ summary: 'پاکسازی تمامی لاگهای پیامک' })
|
|
||||||
clearAllSmsLogs() {
|
|
||||||
return this.settingsService.clearAllSmsLogs();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { SmsService, SmsLogQuery } from '../common/services/sms.service';
|
|
||||||
|
|
||||||
export class ScientificTermData {
|
export class ScientificTermData {
|
||||||
term?: string;
|
term?: string;
|
||||||
@ -11,10 +10,7 @@ export class ScientificTermData {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SettingsService {
|
export class SettingsService {
|
||||||
constructor(
|
constructor(private prisma: PrismaService) {}
|
||||||
private prisma: PrismaService,
|
|
||||||
private smsService: SmsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getUiTexts() {
|
async getUiTexts() {
|
||||||
const [uiTexts, settings] = await Promise.all([
|
const [uiTexts, settings] = await Promise.all([
|
||||||
@ -86,45 +82,4 @@ export class SettingsService {
|
|||||||
create: { key, category, value },
|
create: { key, category, value },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSmsSettings() {
|
|
||||||
return this.smsService.getSmsConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSmsSettings(value: Prisma.InputJsonValue) {
|
|
||||||
const key = 'sms_config';
|
|
||||||
return this.prisma.setting.upsert({
|
|
||||||
where: { key },
|
|
||||||
update: { category: 'sms', value },
|
|
||||||
create: { key, category: 'sms', value },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async testSms(targetPhone: string, patternId?: number, args?: string[]) {
|
|
||||||
return this.smsService.testSms(targetPhone, patternId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPatterns() {
|
|
||||||
return this.smsService.getPatterns();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addPattern(title: string, body: string, blackListId?: number) {
|
|
||||||
return this.smsService.addPattern(title, body, blackListId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async editPattern(bodyId: number, body: string) {
|
|
||||||
return this.smsService.editPattern(bodyId, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSmsLogs(query: SmsLogQuery) {
|
|
||||||
return this.smsService.getSmsLogs(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSmsLog(id: string) {
|
|
||||||
return this.smsService.deleteSmsLog(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearAllSmsLogs() {
|
|
||||||
return this.smsService.clearAllSmsLogs();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders, MessageSquare, Receipt, CreditCard } from 'lucide-react';
|
import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders } from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { useAdminAuthStore } from '../store/adminAuthStore';
|
import { useAdminAuthStore } from '../store/adminAuthStore';
|
||||||
|
|
||||||
@ -16,7 +16,6 @@ const menuGroups = [
|
|||||||
title: 'فروشگاه و تخصصی',
|
title: 'فروشگاه و تخصصی',
|
||||||
items: [
|
items: [
|
||||||
{ icon: ShoppingCart, label: 'سفارشات', path: '/orders' },
|
{ icon: ShoppingCart, label: 'سفارشات', path: '/orders' },
|
||||||
{ icon: Receipt, label: 'تراکنشها و لاگ پرداخت', path: '/transactions' },
|
|
||||||
{ icon: Package, label: 'محصولات', path: '/products' },
|
{ icon: Package, label: 'محصولات', path: '/products' },
|
||||||
{ icon: FolderTree, label: 'دستهبندیها', path: '/categories' },
|
{ icon: FolderTree, label: 'دستهبندیها', path: '/categories' },
|
||||||
{ icon: Tag, label: 'کدهای تخفیف', path: '/coupons' },
|
{ icon: Tag, label: 'کدهای تخفیف', path: '/coupons' },
|
||||||
@ -48,7 +47,6 @@ const menuGroups = [
|
|||||||
title: 'سیستم و تنظیمات',
|
title: 'سیستم و تنظیمات',
|
||||||
items: [
|
items: [
|
||||||
{ icon: Settings, label: 'تنظیمات کلی', path: '/settings' },
|
{ icon: Settings, label: 'تنظیمات کلی', path: '/settings' },
|
||||||
{ icon: MessageSquare, label: 'تنظیمات پیامک', path: '/settings/sms' },
|
|
||||||
{ icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' },
|
{ icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' },
|
||||||
{ icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' },
|
{ icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' },
|
||||||
{ icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' },
|
{ icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' },
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2 } from 'lucide-react';
|
||||||
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2, MessageSquare } from 'lucide-react';
|
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
@ -107,15 +106,6 @@ export default function Settings() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت پارامترهای اصلی و سیستمی کانینا</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت پارامترهای اصلی و سیستمی کانینا</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<Link
|
|
||||||
to="/settings/sms"
|
|
||||||
className="bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 text-xs font-bold px-4 py-2.5 rounded-xl transition-all flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-4 h-4 text-purple-600" />
|
|
||||||
تنظیمات درگاه پیامک (MeliPayamak)
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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 SeoSettingsPage = lazy(() => import('../pages/SeoSettingsPage'));
|
||||||
const FinancialSettingsPage = lazy(() => import('../pages/FinancialSettingsPage'));
|
const FinancialSettingsPage = lazy(() => import('../pages/FinancialSettingsPage'));
|
||||||
const SystemSettingsPage = lazy(() => import('../pages/SystemSettingsPage'));
|
const SystemSettingsPage = lazy(() => import('../pages/SystemSettingsPage'));
|
||||||
const SmsSettingsPage = lazy(() => import('../pages/SmsSettingsPage'));
|
|
||||||
const Transactions = lazy(() => import('../pages/Transactions'));
|
|
||||||
|
|
||||||
export interface AdminRouteConfig {
|
export interface AdminRouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@ -58,10 +56,8 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'users/*', element: <Users /> },
|
{ path: 'users/*', element: <Users /> },
|
||||||
{ path: 'products/*', element: <Products /> },
|
{ path: 'products/*', element: <Products /> },
|
||||||
{ path: 'orders/*', element: <Orders /> },
|
{ path: 'orders/*', element: <Orders /> },
|
||||||
{ path: 'transactions/*', element: <Transactions /> },
|
|
||||||
{ path: 'coupons/*', element: <Coupons /> },
|
{ path: 'coupons/*', element: <Coupons /> },
|
||||||
{ path: 'settings', element: <Settings /> },
|
{ path: 'settings', element: <Settings /> },
|
||||||
{ path: 'settings/sms', element: <SmsSettingsPage /> },
|
|
||||||
{ path: 'settings/seo', element: <SeoSettingsPage /> },
|
{ path: 'settings/seo', element: <SeoSettingsPage /> },
|
||||||
{ path: 'settings/financial', element: <FinancialSettingsPage /> },
|
{ path: 'settings/financial', element: <FinancialSettingsPage /> },
|
||||||
{ path: 'settings/system', element: <SystemSettingsPage /> },
|
{ path: 'settings/system', element: <SystemSettingsPage /> },
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"0": "AdminService",
|
"0": "AdminService",
|
||||||
"1": "pets/pets.controller.ts",
|
"1": "Pet Management Controller",
|
||||||
"2": "Roles",
|
"2": "SettingsController",
|
||||||
"3": "UsersService",
|
"3": "UsersService",
|
||||||
"4": "CmsController",
|
"4": "CMS Content Management",
|
||||||
"5": "BannersService",
|
"5": "OrdersService",
|
||||||
"6": "auth.controller.ts",
|
"6": "auth.controller.ts",
|
||||||
"7": "app.module.ts",
|
"7": "app.module.ts",
|
||||||
"8": "CreateVideoDto",
|
"8": "CreateVideoDto",
|
||||||
@ -12,32 +12,32 @@
|
|||||||
"10": "SmartAdvisor.tsx",
|
"10": "SmartAdvisor.tsx",
|
||||||
"11": "compilerOptions",
|
"11": "compilerOptions",
|
||||||
"12": "toPersian",
|
"12": "toPersian",
|
||||||
"13": "SmsService",
|
"13": "Media and Blog UI Components",
|
||||||
"14": "ProductsService",
|
"14": "ProductsService",
|
||||||
"15": "lib/services/api.ts",
|
"15": "lib/services/api.ts",
|
||||||
"16": "eslint",
|
"16": "devDependencies",
|
||||||
"17": "useSettingsStore",
|
"17": "useSettingsStore",
|
||||||
"18": "AppModule",
|
"18": "AppModule",
|
||||||
"19": "B2B Inquiry Controller",
|
"19": "B2B Inquiry Controller",
|
||||||
"20": "Category Management Controller",
|
"20": "Category Management Controller",
|
||||||
"21": "auth.service.ts",
|
"21": "Banner Management Controller",
|
||||||
"22": "MediaController",
|
"22": "Media Upload Controller",
|
||||||
"23": "Ingredient Management Controller",
|
"23": "Ingredient Management Controller",
|
||||||
"24": "adminRoutes.tsx",
|
"24": "Admin Dashboard Components",
|
||||||
"25": "admin.module.ts",
|
"25": "admin.module.ts",
|
||||||
"26": "ConfirmModal.tsx",
|
"26": "Pet DTOs and Controller",
|
||||||
"27": "Prescription Review Controller",
|
"27": "Prescription Review Controller",
|
||||||
"28": "Smart Advisor Controller",
|
"28": "Smart Advisor Controller",
|
||||||
"29": "Testimonials Management Controller",
|
"29": "Testimonials Management Controller",
|
||||||
"30": "src/services/api.ts",
|
"30": "Admin Sidebar and Layout",
|
||||||
"31": "Spinner.tsx",
|
"31": "Admin Settings Managers",
|
||||||
"32": "useCartStore",
|
"32": "useCartStore",
|
||||||
"33": "ZibalService",
|
"33": "ZibalService",
|
||||||
"34": "main.ts",
|
"34": "main.ts",
|
||||||
"35": "ContactService",
|
"35": "ContactService",
|
||||||
"36": "Backend TypeScript Config",
|
"36": "Backend TypeScript Config",
|
||||||
"37": "App TypeScript Config",
|
"37": "App TypeScript Config",
|
||||||
"38": "Coupons.tsx",
|
"38": "CMS and Media Modals",
|
||||||
"39": "dependencies",
|
"39": "dependencies",
|
||||||
"40": "Node TypeScript Config",
|
"40": "Node TypeScript Config",
|
||||||
"41": "Admin Blog Controller",
|
"41": "Admin Blog Controller",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"43": "devDependencies",
|
"43": "devDependencies",
|
||||||
"44": "Generic CRUD Controller",
|
"44": "Generic CRUD Controller",
|
||||||
"45": "seo.module.ts",
|
"45": "seo.module.ts",
|
||||||
"46": "OrdersController",
|
"46": "Coupon Management UI",
|
||||||
"47": "Project Build Scripts",
|
"47": "Project Build Scripts",
|
||||||
"48": "Home Data Module",
|
"48": "Home Data Module",
|
||||||
"49": "devDependencies",
|
"49": "devDependencies",
|
||||||
@ -61,19 +61,19 @@
|
|||||||
"59": "PrismaService",
|
"59": "PrismaService",
|
||||||
"60": "Jest Testing Config",
|
"60": "Jest Testing Config",
|
||||||
"61": "PaginationDto",
|
"61": "PaginationDto",
|
||||||
"62": "SettingsService",
|
"62": "BlogsController",
|
||||||
"63": "FE-001",
|
"63": "FE-001",
|
||||||
"64": "payment.module.ts",
|
"64": "pagination.dto.ts",
|
||||||
"65": "rules/graphify.md",
|
"65": "rules/graphify.md",
|
||||||
"66": "App Health Controller",
|
"66": "App Health Controller",
|
||||||
"67": "dependencies",
|
"67": "dependencies",
|
||||||
"68": "OrdersService",
|
"68": "Pet Business Logic",
|
||||||
"69": "Integrity Validation Scripts",
|
"69": "Integrity Validation Scripts",
|
||||||
"70": "Admin Panel Package Config",
|
"70": "Admin Panel Package Config",
|
||||||
"71": "Analytics and Report Charts",
|
"71": "Analytics and Report Charts",
|
||||||
"72": "Task Orchestration Scripts",
|
"72": "Task Orchestration Scripts",
|
||||||
"73": "Backend Package Config",
|
"73": "Backend Package Config",
|
||||||
"74": "BlogsController",
|
"74": "Health Log DTOs",
|
||||||
"75": "React Error Boundary",
|
"75": "React Error Boundary",
|
||||||
"76": "Application Package Config",
|
"76": "Application Package Config",
|
||||||
"77": "WikiController",
|
"77": "WikiController",
|
||||||
@ -92,7 +92,7 @@
|
|||||||
"90": "Blog Listing Page",
|
"90": "Blog Listing Page",
|
||||||
"91": "Blog Post Detail Page",
|
"91": "Blog Post Detail Page",
|
||||||
"92": "@types/node",
|
"92": "@types/node",
|
||||||
"93": "devDependencies",
|
"93": "typescript",
|
||||||
"94": "UI Text Seeding",
|
"94": "UI Text Seeding",
|
||||||
"95": "Wiki Terms Seeding",
|
"95": "Wiki Terms Seeding",
|
||||||
"96": "Blog Management DTOs",
|
"96": "Blog Management DTOs",
|
||||||
@ -102,7 +102,7 @@
|
|||||||
"100": "Network Status Banner",
|
"100": "Network Status Banner",
|
||||||
"101": "Docker Deployment Scripts",
|
"101": "Docker Deployment Scripts",
|
||||||
"102": "DB-001",
|
"102": "DB-001",
|
||||||
"103": "BlogsService",
|
"103": "typescript-eslint",
|
||||||
"104": "Database Migration Scripts",
|
"104": "Database Migration Scripts",
|
||||||
"105": "Scientific Terms Schema",
|
"105": "Scientific Terms Schema",
|
||||||
"106": "Blog Data Seeding",
|
"106": "Blog Data Seeding",
|
||||||
@ -116,10 +116,10 @@
|
|||||||
"114": "Manifest Data Generation",
|
"114": "Manifest Data Generation",
|
||||||
"115": "Honest Manifest Synchronization",
|
"115": "Honest Manifest Synchronization",
|
||||||
"116": "Manifest Entry Synchronization",
|
"116": "Manifest Entry Synchronization",
|
||||||
"117": "WikiService",
|
"117": "Pet Reminder DTOs",
|
||||||
"118": "TS-001",
|
"118": "TS-001",
|
||||||
"119": "TEST-001",
|
"119": "TEST-001",
|
||||||
"120": "ValidateCouponDto",
|
"120": "@tailwindcss/postcss",
|
||||||
"121": "@types/react-dom",
|
"121": "@types/react-dom",
|
||||||
"122": "Admin Panel TSConfig",
|
"122": "Admin Panel TSConfig",
|
||||||
"123": "About Page Component",
|
"123": "About Page Component",
|
||||||
@ -129,19 +129,19 @@
|
|||||||
"127": "DEVOPS-001",
|
"127": "DEVOPS-001",
|
||||||
"128": "DOC-001",
|
"128": "DOC-001",
|
||||||
"129": "What You Must Do When Invoked",
|
"129": "What You Must Do When Invoked",
|
||||||
"130": "JwtAuthGuard",
|
"130": "Roles",
|
||||||
"131": "راهنمای تست سیستم (Software Testing)",
|
"131": "راهنمای تست سیستم (Software Testing)",
|
||||||
"132": "Role & Core Objective",
|
"132": "Role & Core Objective",
|
||||||
"133": "Required Review Group Closures",
|
"133": "Required Review Group Closures",
|
||||||
"134": "Operational Rules & Boundaries",
|
"134": "Operational Rules & Boundaries",
|
||||||
"135": "Operational Rules & Boundaries",
|
"135": "Operational Rules & Boundaries",
|
||||||
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||||
"137": "@nestjs/cli",
|
"137": "NestJS CLI Tooling",
|
||||||
"138": "Operational Rules & Boundaries",
|
"138": "Operational Rules & Boundaries",
|
||||||
"139": "Operational Rules & Boundaries",
|
"139": "Operational Rules & Boundaries",
|
||||||
"140": "Bcrypt Type Definitions",
|
"140": "Bcrypt Type Definitions",
|
||||||
"141": "bcryptjs",
|
"141": "Passport JWT Type Definitions",
|
||||||
"142": "helmet",
|
"142": "Supertest Type Definitions",
|
||||||
"143": "Blog Entity Model",
|
"143": "Blog Entity Model",
|
||||||
"144": "Home Entity Model",
|
"144": "Home Entity Model",
|
||||||
"145": "Wiki Entity Model",
|
"145": "Wiki Entity Model",
|
||||||
@ -237,7 +237,7 @@
|
|||||||
"235": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
"235": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
||||||
"236": "Compiler Diagnostic Dispositions",
|
"236": "Compiler Diagnostic Dispositions",
|
||||||
"237": "@eslint/eslintrc",
|
"237": "@eslint/eslintrc",
|
||||||
"238": "js-yaml",
|
"238": "eslint-plugin-prettier",
|
||||||
"239": "globals",
|
"239": "globals",
|
||||||
"240": "prettier",
|
"240": "prettier",
|
||||||
"241": "prisma",
|
"241": "prisma",
|
||||||
@ -263,25 +263,5 @@
|
|||||||
"261": "shabnam-font-v5.0.1/CHANGELOG.md",
|
"261": "shabnam-font-v5.0.1/CHANGELOG.md",
|
||||||
"262": "application/CLAUDE.md",
|
"262": "application/CLAUDE.md",
|
||||||
"263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
"263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
"264": "tailwindcss",
|
"264": "tailwindcss"
|
||||||
"265": "typescript-eslint",
|
|
||||||
"266": "@nestjs/core",
|
|
||||||
"267": "@nestjs/jwt",
|
|
||||||
"268": "@nestjs/swagger",
|
|
||||||
"269": "@nestjs/throttler",
|
|
||||||
"270": "passport-jwt",
|
|
||||||
"271": "@prisma/client",
|
|
||||||
"272": "swagger-ui-express",
|
|
||||||
"273": "AGENTS.md",
|
|
||||||
"274": "eslint-config-prettier",
|
|
||||||
"275": "@eslint/js",
|
|
||||||
"276": "jest",
|
|
||||||
"277": "@nestjs/schematics",
|
|
||||||
"278": "@nestjs/testing",
|
|
||||||
"279": "source-map-support",
|
|
||||||
"280": "ts-jest",
|
|
||||||
"281": "tsconfig-paths",
|
|
||||||
"282": "@types/bcryptjs",
|
|
||||||
"283": "typescript-eslint",
|
|
||||||
"284": "globals"
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,79 +1,79 @@
|
|||||||
{
|
{
|
||||||
"0": "AdminService",
|
"0": "AdminService",
|
||||||
"1": "PetsController",
|
"1": "Pet Management Controller",
|
||||||
"2": "Roles",
|
"2": "Roles",
|
||||||
"3": "UsersService",
|
"3": "UsersService",
|
||||||
"4": "CmsController",
|
"4": "CMS Content Management",
|
||||||
"5": "BannersService",
|
"5": "OrdersController",
|
||||||
"6": "auth.service.ts",
|
"6": "auth.controller.ts",
|
||||||
"7": "app.module.ts",
|
"7": "app.module.ts",
|
||||||
"8": "CreateVideoDto",
|
"8": "CreateVideoDto",
|
||||||
"9": "ProductService",
|
"9": "Public Catalog Pages",
|
||||||
"10": "SmartAdvisor.tsx",
|
"10": "Client Layout and Auth Modals",
|
||||||
"11": "compilerOptions",
|
"11": "Prisma and TypeScript Config",
|
||||||
"12": "toPersian",
|
"12": "Checkout and Dashboard Components",
|
||||||
"13": "pets/pets.controller.ts",
|
"13": "Media and Blog UI Components",
|
||||||
"14": "ProductsService",
|
"14": "ProductsService",
|
||||||
"15": "lib/services/api.ts",
|
"15": "Contact and Prescription Features",
|
||||||
"16": "devDependencies",
|
"16": "Development Tooling Config",
|
||||||
"17": "useSettingsStore",
|
"17": "Home Page Client Components",
|
||||||
"18": "MetricsController",
|
"18": "AppModule",
|
||||||
"19": "B2B Inquiry Controller",
|
"19": "B2B Inquiry Controller",
|
||||||
"20": "Category Management Controller",
|
"20": "Category Management Controller",
|
||||||
"21": "RedisService",
|
"21": "Banner Management Controller",
|
||||||
"22": "MediaController",
|
"22": "Media Upload Controller",
|
||||||
"23": "Ingredient Management Controller",
|
"23": "Ingredient Management Controller",
|
||||||
"24": "adminRoutes.tsx",
|
"24": "Admin Dashboard Components",
|
||||||
"25": "admin.module.ts",
|
"25": "ReportsController",
|
||||||
"26": "api",
|
"26": "Pet DTOs and Controller",
|
||||||
"27": "Prescription Review Controller",
|
"27": "Prescription Review Controller",
|
||||||
"28": "Smart Advisor Controller",
|
"28": "Smart Advisor Controller",
|
||||||
"29": "Testimonials Management Controller",
|
"29": "Testimonials Management Controller",
|
||||||
"30": "src/services/api.ts",
|
"30": "Admin Sidebar and Layout",
|
||||||
"31": "Spinner.tsx",
|
"31": "Admin Settings Managers",
|
||||||
"32": "useCartStore",
|
"32": "User Profile and Success Pages",
|
||||||
"33": "ZibalService",
|
"33": "auth.service.ts",
|
||||||
"34": "main.ts",
|
"34": "main.ts",
|
||||||
"35": "payment.controller.ts",
|
"35": "ContactService",
|
||||||
"36": "Backend TypeScript Config",
|
"36": "Backend TypeScript Config",
|
||||||
"37": "App TypeScript Config",
|
"37": "App TypeScript Config",
|
||||||
"38": "ConfirmModal.tsx",
|
"38": "CMS and Media Modals",
|
||||||
"39": "dependencies",
|
"39": "dependencies",
|
||||||
"40": "Node TypeScript Config",
|
"40": "Node TypeScript Config",
|
||||||
"41": "Admin Blog Controller",
|
"41": "Admin Blog Controller",
|
||||||
"42": "WholesaleService",
|
"42": "WholesaleController",
|
||||||
"43": "devDependencies",
|
"43": "Frontend Testing and Styles",
|
||||||
"44": "Generic CRUD Controller",
|
"44": "Generic CRUD Controller",
|
||||||
"45": "seo.module.ts",
|
"45": "seo.module.ts",
|
||||||
"46": "Coupon Management UI",
|
"46": "Coupon Management UI",
|
||||||
"47": "Project Build Scripts",
|
"47": "Project Build Scripts",
|
||||||
"48": "Home Data Module",
|
"48": "Home Data Module",
|
||||||
"49": "devDependencies",
|
"49": "Linting and PostCSS Config",
|
||||||
"50": "Database Seeding Logic",
|
"50": "Database Seeding Logic",
|
||||||
"51": "Prisma Database Migrations",
|
"51": "Prisma Database Migrations",
|
||||||
"52": "dependencies",
|
"52": "Frontend Core Dependencies",
|
||||||
"53": "UI Skeleton and Tables",
|
"53": "UI Skeleton and Tables",
|
||||||
"54": "components/Skeleton.tsx",
|
"54": "Shop Loading States",
|
||||||
"55": "BE-001",
|
"55": "Product Detail Page",
|
||||||
"56": "VetGallery.tsx",
|
"56": "Video Gallery Components",
|
||||||
"57": "NPM Lifecycle Scripts",
|
"57": "NPM Lifecycle Scripts",
|
||||||
"58": "Pet Management API",
|
"58": "Pet Management API",
|
||||||
"59": "PrismaService",
|
"59": "PrismaService",
|
||||||
"60": "Jest Testing Config",
|
"60": "Jest Testing Config",
|
||||||
"61": "PaginationDto",
|
"61": "PaginationDto",
|
||||||
"62": "WikiController",
|
"62": "BlogsController",
|
||||||
"63": "FE-001",
|
"63": "WholesaleApplyDto",
|
||||||
"64": "PetsService",
|
"64": "SmsService",
|
||||||
"65": "rules/graphify.md",
|
"65": "rules/graphify.md",
|
||||||
"66": "App Health Controller",
|
"66": "App Health Controller",
|
||||||
"67": "dependencies",
|
"67": "Frontend UI Dependencies",
|
||||||
"68": "OrdersService",
|
"68": "Pet Business Logic",
|
||||||
"69": "Integrity Validation Scripts",
|
"69": "Integrity Validation Scripts",
|
||||||
"70": "Admin Panel Package Config",
|
"70": "Admin Panel Package Config",
|
||||||
"71": "Analytics and Report Charts",
|
"71": "Analytics and Report Charts",
|
||||||
"72": "Task Orchestration Scripts",
|
"72": "Task Orchestration Scripts",
|
||||||
"73": "Backend Package Config",
|
"73": "Backend Package Config",
|
||||||
"74": "CreateHealthLogDto",
|
"74": "Health Log DTOs",
|
||||||
"75": "React Error Boundary",
|
"75": "React Error Boundary",
|
||||||
"76": "Application Package Config",
|
"76": "Application Package Config",
|
||||||
"77": ".findAll",
|
"77": ".findAll",
|
||||||
@ -81,7 +81,7 @@
|
|||||||
"79": "VPN Utility Scripts",
|
"79": "VPN Utility Scripts",
|
||||||
"80": "NestJS CLI Config",
|
"80": "NestJS CLI Config",
|
||||||
"81": "Seed TypeScript Config",
|
"81": "Seed TypeScript Config",
|
||||||
"82": "ADM-001",
|
"82": "Order Business Logic",
|
||||||
"83": "Browser Utility Scripts",
|
"83": "Browser Utility Scripts",
|
||||||
"84": "Dev Server Startup",
|
"84": "Dev Server Startup",
|
||||||
"85": "Architectural Audit Findings",
|
"85": "Architectural Audit Findings",
|
||||||
@ -91,8 +91,8 @@
|
|||||||
"89": "Evidence Validation Scripts",
|
"89": "Evidence Validation Scripts",
|
||||||
"90": "Blog Listing Page",
|
"90": "Blog Listing Page",
|
||||||
"91": "Blog Post Detail Page",
|
"91": "Blog Post Detail Page",
|
||||||
"92": "@types/node",
|
"92": "Node Type Definitions",
|
||||||
"93": "typescript",
|
"93": "TypeScript Language",
|
||||||
"94": "UI Text Seeding",
|
"94": "UI Text Seeding",
|
||||||
"95": "Wiki Terms Seeding",
|
"95": "Wiki Terms Seeding",
|
||||||
"96": "Blog Management DTOs",
|
"96": "Blog Management DTOs",
|
||||||
@ -101,8 +101,8 @@
|
|||||||
"99": "Wiki Page Routing",
|
"99": "Wiki Page Routing",
|
||||||
"100": "Network Status Banner",
|
"100": "Network Status Banner",
|
||||||
"101": "Docker Deployment Scripts",
|
"101": "Docker Deployment Scripts",
|
||||||
"102": "DB-001",
|
"102": "ESLint Core Configuration",
|
||||||
"103": "CreateReminderDto",
|
"103": "TypeScript ESLint Support",
|
||||||
"104": "Database Migration Scripts",
|
"104": "Database Migration Scripts",
|
||||||
"105": "Scientific Terms Schema",
|
"105": "Scientific Terms Schema",
|
||||||
"106": "Blog Data Seeding",
|
"106": "Blog Data Seeding",
|
||||||
@ -116,29 +116,29 @@
|
|||||||
"114": "Manifest Data Generation",
|
"114": "Manifest Data Generation",
|
||||||
"115": "Honest Manifest Synchronization",
|
"115": "Honest Manifest Synchronization",
|
||||||
"116": "Manifest Entry Synchronization",
|
"116": "Manifest Entry Synchronization",
|
||||||
"117": "Videos.tsx",
|
"117": "Pet Reminder DTOs",
|
||||||
"118": "TS-001",
|
"118": "React DOM Library",
|
||||||
"119": "TEST-001",
|
"119": "Zustand State Management",
|
||||||
"120": "@tailwindcss/postcss",
|
"120": "Tailwind PostCSS Plugin",
|
||||||
"121": "@types/react-dom",
|
"121": "React DOM Type Definitions",
|
||||||
"122": "Admin Panel TSConfig",
|
"122": "Admin Panel TSConfig",
|
||||||
"123": "About Page Component",
|
"123": "About Page Component",
|
||||||
"124": "Privacy Page Component",
|
"124": "Privacy Page Component",
|
||||||
"125": "Next.js Security Configuration",
|
"125": "Next.js Security Configuration",
|
||||||
"126": "Typography and Font Assets",
|
"126": "Typography and Font Assets",
|
||||||
"127": "DEVOPS-001",
|
"127": "Bcrypt Password Hashing",
|
||||||
"128": "DOC-001",
|
"128": "Helmet Security Middleware",
|
||||||
"129": "What You Must Do When Invoked",
|
"129": "What You Must Do When Invoked",
|
||||||
"130": "RolesGuard",
|
"130": "NestJS Core Framework",
|
||||||
"131": "راهنمای تست سیستم (Software Testing)",
|
"131": "NestJS JWT Authentication",
|
||||||
"132": "Role & Core Objective",
|
"132": "NestJS Swagger Documentation",
|
||||||
"133": "Required Review Group Closures",
|
"133": "NestJS Rate Limiting",
|
||||||
"134": "Operational Rules & Boundaries",
|
"134": "Passport JWT Strategy",
|
||||||
"135": "Operational Rules & Boundaries",
|
"135": "Prisma Client Library",
|
||||||
"136": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
"136": "Swagger UI Express",
|
||||||
"137": "@nestjs/cli",
|
"137": "NestJS CLI Tooling",
|
||||||
"138": "Operational Rules & Boundaries",
|
"138": "NestJS Testing Utilities",
|
||||||
"139": "Operational Rules & Boundaries",
|
"139": "TypeScript Jest Integration",
|
||||||
"140": "Bcrypt Type Definitions",
|
"140": "Bcrypt Type Definitions",
|
||||||
"141": "Passport JWT Type Definitions",
|
"141": "Passport JWT Type Definitions",
|
||||||
"142": "Supertest Type Definitions",
|
"142": "Supertest Type Definitions",
|
||||||
@ -176,7 +176,7 @@
|
|||||||
"174": "Production Docker Setup",
|
"174": "Production Docker Setup",
|
||||||
"175": "Staging Docker Setup",
|
"175": "Staging Docker Setup",
|
||||||
"176": ".agents/workflows/graphify.md",
|
"176": ".agents/workflows/graphify.md",
|
||||||
"177": "Role & Core Objective",
|
"177": "BlogsService",
|
||||||
"178": "graphify reference: extra exports and benchmark",
|
"178": "graphify reference: extra exports and benchmark",
|
||||||
"179": "graphify reference: query, path, explain",
|
"179": "graphify reference: query, path, explain",
|
||||||
"180": "graphify reference: add a URL and watch a folder",
|
"180": "graphify reference: add a URL and watch a folder",
|
||||||
@ -184,87 +184,12 @@
|
|||||||
"182": "graphify reference: incremental update and cluster-only",
|
"182": "graphify reference: incremental update and cluster-only",
|
||||||
"183": "graphify reference: GitHub clone and cross-repo merge",
|
"183": "graphify reference: GitHub clone and cross-repo merge",
|
||||||
"184": "graphify reference: transcribe video and audio",
|
"184": "graphify reference: transcribe video and audio",
|
||||||
"185": "Reconciled Audit Roles & Assignments",
|
"185": "AGENTS.md",
|
||||||
"186": "instructions.md",
|
"186": "instructions.md",
|
||||||
"187": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
"187": "js-yaml",
|
||||||
"188": "CLAUDE.md",
|
"188": "CLAUDE.md",
|
||||||
"189": ".claude/CLAUDE.md",
|
"189": ".claude/CLAUDE.md",
|
||||||
"190": "extraction-spec.md",
|
"190": "extraction-spec.md",
|
||||||
"191": "User Login API",
|
"191": "User Login API",
|
||||||
"192": "Developer Standards and Architecture",
|
"192": "Developer Standards and Architecture"
|
||||||
"193": "Deep Audit Summary Report",
|
|
||||||
"194": "Operational Rules & Boundaries",
|
|
||||||
"195": "Comprehensive Change Log",
|
|
||||||
"196": "Operational Rules & Boundaries",
|
|
||||||
"197": "1. Summary of Integrity Repairs Performed",
|
|
||||||
"198": "Operational Rules & Boundaries",
|
|
||||||
"199": "Operational Rules & Boundaries",
|
|
||||||
"200": "Operational Rules & Boundaries",
|
|
||||||
"201": "Vazirmatn Changelog",
|
|
||||||
"202": "Vazirmatn Font فونت وزیرمتن",
|
|
||||||
"203": "Operational Rules & Boundaries",
|
|
||||||
"204": "backend/README.md",
|
|
||||||
"205": "Repository Map",
|
|
||||||
"206": "Sahel-Font",
|
|
||||||
"207": "AuthService",
|
|
||||||
"208": "Sahel-Font",
|
|
||||||
"209": "Role & Core Objective",
|
|
||||||
"210": "exclude",
|
|
||||||
"211": "Phase 2 Final Quality Gate Summary Report",
|
|
||||||
"212": "Task Modifications Log",
|
|
||||||
"213": "Install",
|
|
||||||
"214": "BlogsController",
|
|
||||||
"215": "System Discovery",
|
|
||||||
"216": "Product Requirement Document (PRD)",
|
|
||||||
"217": "Baseline Command Plan & Reconciled Command History",
|
|
||||||
"218": "Architecture Specification",
|
|
||||||
"219": "Project Health Audit Report",
|
|
||||||
"220": "Open Questions",
|
|
||||||
"221": "Final Phase 2 Audit Closure Report",
|
|
||||||
"222": "📝 Active Agent Working Scratchpad",
|
|
||||||
"223": "🔍 Code Health Audit Review (01_auditor)",
|
|
||||||
"224": "Omitted File Inspection Report",
|
|
||||||
"225": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
|
||||||
"226": "Phase 3 Audit Traceability Matrix",
|
|
||||||
"227": "API Contract Specification",
|
|
||||||
"228": "⚙️ Backend Technical Review (05_dev_backend)",
|
|
||||||
"229": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
|
||||||
"230": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
|
||||||
"231": "Raw Finding Verification & Disposition Report",
|
|
||||||
"232": "React + TypeScript + Vite",
|
|
||||||
"233": "application/README.md",
|
|
||||||
"234": "🔒 Security & Performance Review (09_devops_security)",
|
|
||||||
"235": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
|
||||||
"236": "Compiler Diagnostic Dispositions",
|
|
||||||
"237": "@eslint/eslintrc",
|
|
||||||
"238": "eslint-plugin-prettier",
|
|
||||||
"239": "globals",
|
|
||||||
"240": "prettier",
|
|
||||||
"241": "prisma",
|
|
||||||
"242": "supertest",
|
|
||||||
"243": "ts-loader",
|
|
||||||
"244": "ts-node",
|
|
||||||
"245": "@types/express",
|
|
||||||
"246": "@types/jest",
|
|
||||||
"247": "@types/js-yaml",
|
|
||||||
"248": "@types/multer",
|
|
||||||
"249": "eslint-plugin-react-hooks",
|
|
||||||
"250": "eslint-plugin-react-refresh",
|
|
||||||
"251": "tailwindcss",
|
|
||||||
"252": "typescript",
|
|
||||||
"253": "@testing-library/jest-dom",
|
|
||||||
"254": "@testing-library/react",
|
|
||||||
"255": "@types/react",
|
|
||||||
"256": "typescript",
|
|
||||||
"257": "vitest",
|
|
||||||
"258": "reviews/README.md",
|
|
||||||
"259": "axios",
|
|
||||||
"260": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
|
||||||
"261": "shabnam-font-v5.0.1/CHANGELOG.md",
|
|
||||||
"262": "application/CLAUDE.md",
|
|
||||||
"263": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
|
||||||
"264": "tailwindcss",
|
|
||||||
"265": "typescript-eslint",
|
|
||||||
"268": "wholesale.controller.ts",
|
|
||||||
"273": "AGENTS.md"
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,26 +1,26 @@
|
|||||||
# Graph Report - canina (2026-08-16)
|
# Graph Report - canina (2026-08-16)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 463 files · ~678,911 words
|
- 459 files · ~667,716 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- 3234 nodes · 5269 edges · 285 communities (172 shown, 113 thin omitted)
|
- 3167 nodes · 5051 edges · 265 communities (171 shown, 94 thin omitted)
|
||||||
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 186 edges (avg confidence: 0.79)
|
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 160 edges (avg confidence: 0.79)
|
||||||
- Token cost: 0 input · 0 output
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
## Graph Freshness
|
## Graph Freshness
|
||||||
- Built from commit: `8de13513`
|
- Built from commit: `03900e72`
|
||||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||||
- Run `graphify update .` after code changes (no API cost).
|
- Run `graphify update .` after code changes (no API cost).
|
||||||
|
|
||||||
## Community Hubs (Navigation)
|
## Community Hubs (Navigation)
|
||||||
- AdminService
|
- AdminService
|
||||||
- pets/pets.controller.ts
|
- Pet Management Controller
|
||||||
- Roles
|
- SettingsController
|
||||||
- UsersService
|
- UsersService
|
||||||
- CmsController
|
- CMS Content Management
|
||||||
- BannersService
|
- OrdersService
|
||||||
- auth.controller.ts
|
- auth.controller.ts
|
||||||
- app.module.ts
|
- app.module.ts
|
||||||
- CreateVideoDto
|
- CreateVideoDto
|
||||||
@ -28,32 +28,32 @@
|
|||||||
- SmartAdvisor.tsx
|
- SmartAdvisor.tsx
|
||||||
- compilerOptions
|
- compilerOptions
|
||||||
- toPersian
|
- toPersian
|
||||||
- SmsService
|
- Media and Blog UI Components
|
||||||
- ProductsService
|
- ProductsService
|
||||||
- lib/services/api.ts
|
- lib/services/api.ts
|
||||||
- eslint
|
- devDependencies
|
||||||
- useSettingsStore
|
- useSettingsStore
|
||||||
- AppModule
|
- AppModule
|
||||||
- B2B Inquiry Controller
|
- B2B Inquiry Controller
|
||||||
- Category Management Controller
|
- Category Management Controller
|
||||||
- auth.service.ts
|
- Banner Management Controller
|
||||||
- MediaController
|
- Media Upload Controller
|
||||||
- Ingredient Management Controller
|
- Ingredient Management Controller
|
||||||
- adminRoutes.tsx
|
- Admin Dashboard Components
|
||||||
- admin.module.ts
|
- admin.module.ts
|
||||||
- ConfirmModal.tsx
|
- Pet DTOs and Controller
|
||||||
- Prescription Review Controller
|
- Prescription Review Controller
|
||||||
- Smart Advisor Controller
|
- Smart Advisor Controller
|
||||||
- Testimonials Management Controller
|
- Testimonials Management Controller
|
||||||
- src/services/api.ts
|
- Admin Sidebar and Layout
|
||||||
- Spinner.tsx
|
- Admin Settings Managers
|
||||||
- useCartStore
|
- useCartStore
|
||||||
- ZibalService
|
- ZibalService
|
||||||
- main.ts
|
- main.ts
|
||||||
- ContactService
|
- ContactService
|
||||||
- Backend TypeScript Config
|
- Backend TypeScript Config
|
||||||
- App TypeScript Config
|
- App TypeScript Config
|
||||||
- Coupons.tsx
|
- CMS and Media Modals
|
||||||
- dependencies
|
- dependencies
|
||||||
- Node TypeScript Config
|
- Node TypeScript Config
|
||||||
- Admin Blog Controller
|
- Admin Blog Controller
|
||||||
@ -61,7 +61,7 @@
|
|||||||
- devDependencies
|
- devDependencies
|
||||||
- Generic CRUD Controller
|
- Generic CRUD Controller
|
||||||
- seo.module.ts
|
- seo.module.ts
|
||||||
- OrdersController
|
- Coupon Management UI
|
||||||
- Project Build Scripts
|
- Project Build Scripts
|
||||||
- Home Data Module
|
- Home Data Module
|
||||||
- devDependencies
|
- devDependencies
|
||||||
@ -77,19 +77,19 @@
|
|||||||
- PrismaService
|
- PrismaService
|
||||||
- Jest Testing Config
|
- Jest Testing Config
|
||||||
- PaginationDto
|
- PaginationDto
|
||||||
- SettingsService
|
- BlogsController
|
||||||
- FE-001
|
- FE-001
|
||||||
- payment.module.ts
|
- pagination.dto.ts
|
||||||
- rules/graphify.md
|
- rules/graphify.md
|
||||||
- App Health Controller
|
- App Health Controller
|
||||||
- dependencies
|
- dependencies
|
||||||
- OrdersService
|
- Pet Business Logic
|
||||||
- Integrity Validation Scripts
|
- Integrity Validation Scripts
|
||||||
- Admin Panel Package Config
|
- Admin Panel Package Config
|
||||||
- Analytics and Report Charts
|
- Analytics and Report Charts
|
||||||
- Task Orchestration Scripts
|
- Task Orchestration Scripts
|
||||||
- Backend Package Config
|
- Backend Package Config
|
||||||
- BlogsController
|
- Health Log DTOs
|
||||||
- React Error Boundary
|
- React Error Boundary
|
||||||
- Application Package Config
|
- Application Package Config
|
||||||
- WikiController
|
- WikiController
|
||||||
@ -108,7 +108,7 @@
|
|||||||
- Blog Listing Page
|
- Blog Listing Page
|
||||||
- Blog Post Detail Page
|
- Blog Post Detail Page
|
||||||
- @types/node
|
- @types/node
|
||||||
- devDependencies
|
- typescript
|
||||||
- UI Text Seeding
|
- UI Text Seeding
|
||||||
- Wiki Terms Seeding
|
- Wiki Terms Seeding
|
||||||
- Blog Management DTOs
|
- Blog Management DTOs
|
||||||
@ -118,7 +118,7 @@
|
|||||||
- Network Status Banner
|
- Network Status Banner
|
||||||
- Docker Deployment Scripts
|
- Docker Deployment Scripts
|
||||||
- DB-001
|
- DB-001
|
||||||
- BlogsService
|
- typescript-eslint
|
||||||
- Database Migration Scripts
|
- Database Migration Scripts
|
||||||
- Scientific Terms Schema
|
- Scientific Terms Schema
|
||||||
- Blog Data Seeding
|
- Blog Data Seeding
|
||||||
@ -132,10 +132,10 @@
|
|||||||
- Manifest Data Generation
|
- Manifest Data Generation
|
||||||
- Honest Manifest Synchronization
|
- Honest Manifest Synchronization
|
||||||
- Manifest Entry Synchronization
|
- Manifest Entry Synchronization
|
||||||
- WikiService
|
- Pet Reminder DTOs
|
||||||
- TS-001
|
- TS-001
|
||||||
- TEST-001
|
- TEST-001
|
||||||
- ValidateCouponDto
|
- @tailwindcss/postcss
|
||||||
- @types/react-dom
|
- @types/react-dom
|
||||||
- Admin Panel TSConfig
|
- Admin Panel TSConfig
|
||||||
- About Page Component
|
- About Page Component
|
||||||
@ -145,19 +145,19 @@
|
|||||||
- DEVOPS-001
|
- DEVOPS-001
|
||||||
- DOC-001
|
- DOC-001
|
||||||
- What You Must Do When Invoked
|
- What You Must Do When Invoked
|
||||||
- JwtAuthGuard
|
- Roles
|
||||||
- راهنمای تست سیستم (Software Testing)
|
- راهنمای تست سیستم (Software Testing)
|
||||||
- Role & Core Objective
|
- Role & Core Objective
|
||||||
- Required Review Group Closures
|
- Required Review Group Closures
|
||||||
- Operational Rules & Boundaries
|
- Operational Rules & Boundaries
|
||||||
- Operational Rules & Boundaries
|
- Operational Rules & Boundaries
|
||||||
- 🏢 AI Software Agency — Master Orchestration Protocol v3
|
- 🏢 AI Software Agency — Master Orchestration Protocol v3
|
||||||
- @nestjs/cli
|
- NestJS CLI Tooling
|
||||||
- Operational Rules & Boundaries
|
- Operational Rules & Boundaries
|
||||||
- Operational Rules & Boundaries
|
- Operational Rules & Boundaries
|
||||||
- Bcrypt Type Definitions
|
- Bcrypt Type Definitions
|
||||||
- bcryptjs
|
- Passport JWT Type Definitions
|
||||||
- helmet
|
- Supertest Type Definitions
|
||||||
- Blog Entity Model
|
- Blog Entity Model
|
||||||
- Home Entity Model
|
- Home Entity Model
|
||||||
- Wiki Entity Model
|
- Wiki Entity Model
|
||||||
@ -246,7 +246,7 @@
|
|||||||
- 👁️ UX & Persona Interface Review (08_visual_qa)
|
- 👁️ UX & Persona Interface Review (08_visual_qa)
|
||||||
- Compiler Diagnostic Dispositions
|
- Compiler Diagnostic Dispositions
|
||||||
- @eslint/eslintrc
|
- @eslint/eslintrc
|
||||||
- js-yaml
|
- eslint-plugin-prettier
|
||||||
- globals
|
- globals
|
||||||
- prettier
|
- prettier
|
||||||
- prisma
|
- prisma
|
||||||
@ -266,38 +266,18 @@
|
|||||||
- @types/react
|
- @types/react
|
||||||
- typescript
|
- typescript
|
||||||
- vitest
|
- vitest
|
||||||
- typescript-eslint
|
|
||||||
- @nestjs/core
|
|
||||||
- @nestjs/jwt
|
|
||||||
- @nestjs/swagger
|
|
||||||
- @nestjs/throttler
|
|
||||||
- passport-jwt
|
|
||||||
- @prisma/client
|
|
||||||
- swagger-ui-express
|
|
||||||
- AGENTS.md
|
|
||||||
- eslint-config-prettier
|
|
||||||
- @eslint/js
|
|
||||||
- jest
|
|
||||||
- @nestjs/schematics
|
|
||||||
- @nestjs/testing
|
|
||||||
- source-map-support
|
|
||||||
- ts-jest
|
|
||||||
- tsconfig-paths
|
|
||||||
- @types/bcryptjs
|
|
||||||
- typescript-eslint
|
|
||||||
- globals
|
|
||||||
|
|
||||||
## God Nodes (most connected - your core abstractions)
|
## God Nodes (most connected - your core abstractions)
|
||||||
1. `PrismaService` - 74 edges
|
1. `PrismaService` - 72 edges
|
||||||
2. `Roles()` - 59 edges
|
2. `Roles()` - 45 edges
|
||||||
3. `PaginationDto` - 39 edges
|
3. `PaginationDto` - 39 edges
|
||||||
4. `SmsService` - 38 edges
|
4. `useSettingsStore` - 33 edges
|
||||||
5. `api` - 34 edges
|
5. `api` - 32 edges
|
||||||
6. `useSettingsStore` - 33 edges
|
6. `AdminService` - 31 edges
|
||||||
7. `AdminService` - 31 edges
|
7. `toPersian()` - 31 edges
|
||||||
8. `toPersian()` - 31 edges
|
8. `AdminController` - 30 edges
|
||||||
9. `AdminController` - 30 edges
|
9. `useCartStore` - 29 edges
|
||||||
10. `useCartStore` - 29 edges
|
10. `JwtAuthGuard` - 27 edges
|
||||||
|
|
||||||
## Surprising Connections (you probably didn't know these)
|
## Surprising Connections (you probably didn't know these)
|
||||||
- `User Profile Photo` --conceptually_related_to--> `User Roles and Capabilities` [INFERRED]
|
- `User Profile Photo` --conceptually_related_to--> `User Roles and Capabilities` [INFERRED]
|
||||||
@ -312,47 +292,47 @@
|
|||||||
backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts
|
backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts
|
||||||
|
|
||||||
## Import Cycles
|
## Import Cycles
|
||||||
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
|
|
||||||
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
|
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
|
||||||
|
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
|
||||||
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
|
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
|
||||||
|
|
||||||
## Hyperedges (group relationships)
|
## Hyperedges (group relationships)
|
||||||
- **Rastikerdar Font Ecosystem** — frontend_application_public_fonts_shabnam_font_v5_0_1_readme, frontend_application_public_fonts_vazirmatn_v33_003_readme, vazir_font [EXTRACTED 0.95]
|
- **Rastikerdar Font Ecosystem** — frontend_application_public_fonts_shabnam_font_v5_0_1_readme, frontend_application_public_fonts_vazirmatn_v33_003_readme, vazir_font [EXTRACTED 0.95]
|
||||||
- **Canina Deployment Stack** — scripts_compose_prod, scripts_compose_stage [EXTRACTED 1.00]
|
- **Canina Deployment Stack** — scripts_compose_prod, scripts_compose_stage [EXTRACTED 1.00]
|
||||||
|
|
||||||
## Communities (285 total, 113 thin omitted)
|
## Communities (265 total, 94 thin omitted)
|
||||||
|
|
||||||
### Community 0 - "AdminService"
|
### Community 0 - "AdminService"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
Nodes (22): AdminController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+14 more)
|
Nodes (22): AdminController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+14 more)
|
||||||
|
|
||||||
### Community 1 - "pets/pets.controller.ts"
|
### Community 1 - "Pet Management Controller"
|
||||||
Cohesion: 0.05
|
Cohesion: 0.17
|
||||||
Nodes (45): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+37 more)
|
Nodes (18): PetsController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+10 more)
|
||||||
|
|
||||||
### Community 2 - "Roles"
|
### Community 2 - "SettingsController"
|
||||||
Cohesion: 0.19
|
Cohesion: 0.13
|
||||||
Nodes (16): Roles(), SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+8 more)
|
Nodes (16): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+8 more)
|
||||||
|
|
||||||
### Community 3 - "UsersService"
|
### Community 3 - "UsersService"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.06
|
||||||
Nodes (33): JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty (+25 more)
|
Nodes (37): AuthModule, Module, JwtPayload, JwtStrategy, Injectable, AddressDto, ApiProperty, ApiPropertyOptional (+29 more)
|
||||||
|
|
||||||
### Community 4 - "CmsController"
|
### Community 4 - "CMS Content Management"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.09
|
||||||
Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
|
Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
|
||||||
|
|
||||||
### Community 5 - "BannersService"
|
### Community 5 - "OrdersService"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.07
|
||||||
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
|
Nodes (29): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+21 more)
|
||||||
|
|
||||||
### Community 6 - "auth.controller.ts"
|
### Community 6 - "auth.controller.ts"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.05
|
||||||
Nodes (39): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+31 more)
|
Nodes (41): AuthController, ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Post (+33 more)
|
||||||
|
|
||||||
### Community 7 - "app.module.ts"
|
### Community 7 - "app.module.ts"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.09
|
||||||
Nodes (25): AuthModule, Module, B2BModule, Module, BannersModule, Module, CmsModule, Module (+17 more)
|
Nodes (28): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+20 more)
|
||||||
|
|
||||||
### Community 8 - "CreateVideoDto"
|
### Community 8 - "CreateVideoDto"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
@ -374,6 +354,10 @@ Nodes (32): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModu
|
|||||||
Cohesion: 0.08
|
Cohesion: 0.08
|
||||||
Nodes (37): ClientLayout(), VerifyContent(), AddressModal(), AddressModalProps, AuthModal(), AuthModalProps, B2BPortal(), BackButton() (+29 more)
|
Nodes (37): ClientLayout(), VerifyContent(), AddressModal(), AddressModalProps, AuthModal(), AuthModalProps, B2BPortal(), BackButton() (+29 more)
|
||||||
|
|
||||||
|
### Community 13 - "Media and Blog UI Components"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (19): Media, MediaSelector(), MediaSelectorProps, Pagination(), PaginationProps, BlogPost, Category, Pet (+11 more)
|
||||||
|
|
||||||
### Community 14 - "ProductsService"
|
### Community 14 - "ProductsService"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.09
|
||||||
Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
|
Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
|
||||||
@ -382,10 +366,18 @@ Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, P
|
|||||||
Cohesion: 0.08
|
Cohesion: 0.08
|
||||||
Nodes (17): metadata, ContactFormClient(), ContactInfoItem, PrescriptionUploadModal(), PrescriptionUploadModalProps, api, ApiErrorPayload, ApiErr (+9 more)
|
Nodes (17): metadata, ContactFormClient(), ContactInfoItem, PrescriptionUploadModal(), PrescriptionUploadModalProps, api, ApiErrorPayload, ApiErr (+9 more)
|
||||||
|
|
||||||
|
### Community 16 - "devDependencies"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (23): devDependencies, eslint, eslint-config-prettier, @eslint/js, jest, @nestjs/schematics, @nestjs/testing, source-map-support (+15 more)
|
||||||
|
|
||||||
### Community 17 - "useSettingsStore"
|
### Community 17 - "useSettingsStore"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
Nodes (30): HomeClient(), HomeClientProps, getHomeData(), Home(), metadata, metadata, EnamadBadge(), Footer() (+22 more)
|
Nodes (30): HomeClient(), HomeClientProps, getHomeData(), Home(), metadata, metadata, EnamadBadge(), Footer() (+22 more)
|
||||||
|
|
||||||
|
### Community 18 - "AppModule"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (7): ApiExcludeController, AppModule, Module, MetricsController, Controller, Get, Res
|
||||||
|
|
||||||
### Community 19 - "B2B Inquiry Controller"
|
### Community 19 - "B2B Inquiry Controller"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+7 more)
|
Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+7 more)
|
||||||
@ -394,29 +386,29 @@ Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controlle
|
|||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more)
|
Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more)
|
||||||
|
|
||||||
### Community 21 - "auth.service.ts"
|
### Community 21 - "Banner Management Controller"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.13
|
||||||
Nodes (10): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, RedisModule, Global, Module (+2 more)
|
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
|
||||||
|
|
||||||
### Community 22 - "MediaController"
|
### Community 22 - "Media Upload Controller"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.11
|
||||||
Nodes (16): MediaController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
|
Nodes (16): MediaController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
|
||||||
|
|
||||||
### Community 23 - "Ingredient Management Controller"
|
### Community 23 - "Ingredient Management Controller"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
||||||
|
|
||||||
### Community 24 - "adminRoutes.tsx"
|
### Community 24 - "Admin Dashboard Components"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.10
|
||||||
Nodes (18): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, PatternItem, SmsConfigState, SmsLogItem (+10 more)
|
Nodes (14): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, GROUP_PAGE_MAP, GROUPS, PAGE_TABS (+6 more)
|
||||||
|
|
||||||
### Community 25 - "admin.module.ts"
|
### Community 25 - "admin.module.ts"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.07
|
||||||
Nodes (14): AdminModule, Module, PetQuery, PetsService, Injectable, ReportsController, ApiBearerAuth, ApiOperation (+6 more)
|
Nodes (20): AdminModule, Module, BlogQuery, BlogsService, Injectable, PetQuery, PetsService, Injectable (+12 more)
|
||||||
|
|
||||||
### Community 26 - "ConfirmModal.tsx"
|
### Community 26 - "Pet DTOs and Controller"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.16
|
||||||
Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Pet, fetchVideosList(), Video, Videos() (+8 more)
|
Nodes (13): CreatePetDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional, IsString (+5 more)
|
||||||
|
|
||||||
### Community 27 - "Prescription Review Controller"
|
### Community 27 - "Prescription Review Controller"
|
||||||
Cohesion: 0.14
|
Cohesion: 0.14
|
||||||
@ -430,21 +422,21 @@ Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body,
|
|||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
|
||||||
|
|
||||||
### Community 30 - "src/services/api.ts"
|
### Community 30 - "Admin Sidebar and Layout"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.17
|
||||||
Nodes (27): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+19 more)
|
Nodes (16): Layout(), ProtectedRoute(), menuGroups, Sidebar(), SidebarProps, SEARCHABLE_PAGES, Topbar(), TopbarProps (+8 more)
|
||||||
|
|
||||||
### Community 31 - "Spinner.tsx"
|
### Community 31 - "Admin Settings Managers"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.09
|
||||||
Nodes (23): Media, MediaSelector(), MediaSelectorProps, Spinner(), BlogPost, Category, Category, Product (+15 more)
|
Nodes (21): Spinner(), B2BManager, BannersManager, FinancialSettingsPage, SeoSettingsPage, SmartAdvisorManager, SystemSettingsPage, TestimonialsManager (+13 more)
|
||||||
|
|
||||||
### Community 32 - "useCartStore"
|
### Community 32 - "useCartStore"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
Nodes (16): metadata, ArchiveProductCard(), ProductCard(), Header(), MENU_ICONS, OrderSuccess(), OrderTracking(), PetProfile() (+8 more)
|
Nodes (16): metadata, ArchiveProductCard(), ProductCard(), Header(), MENU_ICONS, OrderSuccess(), OrderTracking(), PetProfile() (+8 more)
|
||||||
|
|
||||||
### Community 33 - "ZibalService"
|
### Community 33 - "ZibalService"
|
||||||
Cohesion: 0.06
|
Cohesion: 0.07
|
||||||
Nodes (37): AdminTransactionFilterDto, ApiPropertyOptional, IsNumber, IsOptional, IsString, Type, InitiatePaymentDto, InitiateWalletTopupDto (+29 more)
|
Nodes (30): InitiatePaymentDto, InitiateWalletTopupDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+22 more)
|
||||||
|
|
||||||
### Community 34 - "main.ts"
|
### Community 34 - "main.ts"
|
||||||
Cohesion: 0.14
|
Cohesion: 0.14
|
||||||
@ -462,13 +454,13 @@ Nodes (22): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration,
|
|||||||
Cohesion: 0.09
|
Cohesion: 0.09
|
||||||
Nodes (22): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+14 more)
|
Nodes (22): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+14 more)
|
||||||
|
|
||||||
### Community 38 - "Coupons.tsx"
|
### Community 38 - "CMS and Media Modals"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.10
|
||||||
Nodes (15): Pagination(), PaginationProps, Coupon, CouponFormData, CouponModalProps, CouponTarget, Media, Stats (+7 more)
|
Nodes (16): ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, Media, fetchVideosList(), Video, Videos() (+8 more)
|
||||||
|
|
||||||
### Community 39 - "dependencies"
|
### Community 39 - "dependencies"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.05
|
||||||
Nodes (21): dependencies, bcrypt, class-transformer, class-validator, ioredis, @nestjs/common, @nestjs/passport, @nestjs/platform-express (+13 more)
|
Nodes (41): dependencies, bcrypt, bcryptjs, class-transformer, class-validator, helmet, ioredis, js-yaml (+33 more)
|
||||||
|
|
||||||
### Community 40 - "Node TypeScript Config"
|
### Community 40 - "Node TypeScript Config"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
@ -494,9 +486,9 @@ Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, De
|
|||||||
Cohesion: 0.16
|
Cohesion: 0.16
|
||||||
Nodes (10): SeoController, ApiOperation, ApiTags, Controller, Get, Param, SeoModule, Module (+2 more)
|
Nodes (10): SeoController, ApiOperation, ApiTags, Controller, Get, Param, SeoModule, Module (+2 more)
|
||||||
|
|
||||||
### Community 46 - "OrdersController"
|
### Community 46 - "Coupon Management UI"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.25
|
||||||
Nodes (16): OrdersController, ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags (+8 more)
|
Nodes (5): Coupon, CouponFormData, CouponModalProps, CouponTarget, Coupons
|
||||||
|
|
||||||
### Community 47 - "Project Build Scripts"
|
### Community 47 - "Project Build Scripts"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
@ -508,7 +500,7 @@ Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Ge
|
|||||||
|
|
||||||
### Community 49 - "devDependencies"
|
### Community 49 - "devDependencies"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, postcss, @tailwindcss/postcss, @types/node (+11 more)
|
Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, globals, postcss, @types/node (+11 more)
|
||||||
|
|
||||||
### Community 50 - "Database Seeding Logic"
|
### Community 50 - "Database Seeding Logic"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.17
|
||||||
@ -547,8 +539,8 @@ Cohesion: 0.15
|
|||||||
Nodes (11): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+3 more)
|
Nodes (11): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+3 more)
|
||||||
|
|
||||||
### Community 59 - "PrismaService"
|
### Community 59 - "PrismaService"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.06
|
||||||
Nodes (18): ApiExcludeController, CouponTargetInput, PaginationQuery, CategoryQuery, MetricsController, Controller, Get, Res (+10 more)
|
Nodes (21): CouponTargetInput, PaginationQuery, CategoryQuery, AdminLoginInput, LoginInput, RegisterInput, MeliPayamakResponse, SendPatternSmsOptions (+13 more)
|
||||||
|
|
||||||
### Community 60 - "Jest Testing Config"
|
### Community 60 - "Jest Testing Config"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.15
|
||||||
@ -556,19 +548,19 @@ Nodes (13): jest, collectCoverageFrom, coverageDirectory, moduleFileExtensions,
|
|||||||
|
|
||||||
### Community 61 - "PaginationDto"
|
### Community 61 - "PaginationDto"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (13): PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsOptional, IsString, Type, Module (+5 more)
|
Nodes (12): PaginationDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, Type, Module, WikiModule (+4 more)
|
||||||
|
|
||||||
### Community 62 - "SettingsService"
|
### Community 62 - "BlogsController"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.20
|
||||||
Nodes (4): SmsLogQuery, ScientificTermData, SettingsService, Injectable
|
Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable
|
||||||
|
|
||||||
### Community 63 - "FE-001"
|
### Community 63 - "FE-001"
|
||||||
Cohesion: 0.06
|
Cohesion: 0.06
|
||||||
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Architecture Overview & Confirmed Strengths, Category, Completion Statement, Confidence (+23 more)
|
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Architecture Overview & Confirmed Strengths, Category, Completion Statement, Confidence (+23 more)
|
||||||
|
|
||||||
### Community 64 - "payment.module.ts"
|
### Community 64 - "pagination.dto.ts"
|
||||||
Cohesion: 0.16
|
Cohesion: 0.11
|
||||||
Nodes (10): SmsModule, Global, Module, ContactModule, Module, PaymentModule, Module, ZibalInquiryResponse (+2 more)
|
Nodes (10): Injectable, WikiQuery, WikiService, SortOrder, IsNotEmpty, IsNumber, IsString, ValidateCouponDto (+2 more)
|
||||||
|
|
||||||
### Community 66 - "App Health Controller"
|
### Community 66 - "App Health Controller"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
@ -578,10 +570,6 @@ Nodes (5): AppController, Controller, Get, AppService, Injectable
|
|||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
Nodes (17): dependencies, axios, lucide-react, motion, next, react, react-dom, sonner (+9 more)
|
Nodes (17): dependencies, axios, lucide-react, motion, next, react, react-dom, sonner (+9 more)
|
||||||
|
|
||||||
### Community 68 - "OrdersService"
|
|
||||||
Cohesion: 0.12
|
|
||||||
Nodes (15): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+7 more)
|
|
||||||
|
|
||||||
### Community 69 - "Integrity Validation Scripts"
|
### Community 69 - "Integrity Validation Scripts"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.20
|
||||||
Nodes (9): dispSum, errors, invalidNewIds, manifest, report, trackedFiles, uninspected, verifiedIndex (+1 more)
|
Nodes (9): dispSum, errors, invalidNewIds, manifest, report, trackedFiles, uninspected, verifiedIndex (+1 more)
|
||||||
@ -602,9 +590,9 @@ Nodes (8): cmd_next_task(), cmd_progress(), cmd_reset(), cmd_status(), cmd_unblo
|
|||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
Nodes (8): author, description, license, name, prisma, seed, private, version
|
Nodes (8): author, description, license, name, prisma, seed, private, version
|
||||||
|
|
||||||
### Community 74 - "BlogsController"
|
### Community 74 - "Health Log DTOs"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.29
|
||||||
Nodes (7): BlogsController, ApiTags, Controller, BlogsModule, Module, BlogsService, Injectable
|
Nodes (6): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
|
||||||
|
|
||||||
### Community 75 - "React Error Boundary"
|
### Community 75 - "React Error Boundary"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
@ -666,10 +654,6 @@ Nodes (4): Blog(), getBlogs(), metadata, BlogPage()
|
|||||||
Cohesion: 0.60
|
Cohesion: 0.60
|
||||||
Nodes (4): BlogPostPage(), generateMetadata(), getBlog(), revalidate
|
Nodes (4): BlogPostPage(), generateMetadata(), getBlog(), revalidate
|
||||||
|
|
||||||
### Community 93 - "devDependencies"
|
|
||||||
Cohesion: 0.22
|
|
||||||
Nodes (9): devDependencies, eslint-plugin-prettier, @types/passport-jwt, @types/supertest, typescript, typescript, eslint-plugin-prettier, @types/passport-jwt (+1 more)
|
|
||||||
|
|
||||||
### Community 99 - "Wiki Page Routing"
|
### Community 99 - "Wiki Page Routing"
|
||||||
Cohesion: 0.83
|
Cohesion: 0.83
|
||||||
Nodes (3): generateMetadata(), getWikiTerm(), WikiTermPage()
|
Nodes (3): generateMetadata(), getWikiTerm(), WikiTermPage()
|
||||||
@ -682,17 +666,13 @@ Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script
|
|||||||
Cohesion: 0.06
|
Cohesion: 0.06
|
||||||
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Database and Data Integrity Audit Report (+23 more)
|
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Database and Data Integrity Audit Report (+23 more)
|
||||||
|
|
||||||
### Community 103 - "BlogsService"
|
|
||||||
Cohesion: 0.22
|
|
||||||
Nodes (3): BlogQuery, BlogsService, Injectable
|
|
||||||
|
|
||||||
### Community 109 - "Auth Architecture and Planning"
|
### Community 109 - "Auth Architecture and Planning"
|
||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Management, Master Task Backlog (Phase 3.3), Phase 3 Master Execution Plan
|
Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Management, Master Task Backlog (Phase 3.3), Phase 3 Master Execution Plan
|
||||||
|
|
||||||
### Community 117 - "WikiService"
|
### Community 117 - "Pet Reminder DTOs"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.29
|
||||||
Nodes (3): Injectable, WikiQuery, WikiService
|
Nodes (6): CreateReminderDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
|
||||||
|
|
||||||
### Community 118 - "TS-001"
|
### Community 118 - "TS-001"
|
||||||
Cohesion: 0.06
|
Cohesion: 0.06
|
||||||
@ -702,10 +682,6 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
|
|||||||
Cohesion: 0.06
|
Cohesion: 0.06
|
||||||
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Dependencies (+23 more)
|
Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternative Direction, Category, Completion Statement, Confidence, Dependencies (+23 more)
|
||||||
|
|
||||||
### Community 120 - "ValidateCouponDto"
|
|
||||||
Cohesion: 0.50
|
|
||||||
Nodes (4): IsNotEmpty, IsNumber, IsString, ValidateCouponDto
|
|
||||||
|
|
||||||
### Community 126 - "Typography and Font Assets"
|
### Community 126 - "Typography and Font Assets"
|
||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font
|
Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font
|
||||||
@ -722,9 +698,9 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
|
|||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
Nodes (26): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+18 more)
|
Nodes (26): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+18 more)
|
||||||
|
|
||||||
### Community 130 - "JwtAuthGuard"
|
### Community 130 - "Roles"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.21
|
||||||
Nodes (7): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
|
Nodes (8): JwtAuthGuard, Injectable, Roles(), ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
|
||||||
|
|
||||||
### Community 131 - "راهنمای تست سیستم (Software Testing)"
|
### Community 131 - "راهنمای تست سیستم (Software Testing)"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
@ -955,24 +931,24 @@ Cohesion: 0.50
|
|||||||
Nodes (3): Deploy on Vercel, Getting Started, Learn More
|
Nodes (3): Deploy on Vercel, Getting Started, Learn More
|
||||||
|
|
||||||
## Knowledge Gaps
|
## Knowledge Gaps
|
||||||
- **1179 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1174 more)
|
- **1170 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1165 more)
|
||||||
These have ≤1 connection - possible missing edges or undocumented components.
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
- **113 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
- **94 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||||
|
|
||||||
## Suggested Questions
|
## Suggested Questions
|
||||||
_Questions this graph is uniquely positioned to answer:_
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
- **Why does `ApiResponse` connect `src/services/api.ts` to `pets/pets.controller.ts`, `UsersService`, `auth.controller.ts`, `BlogsController`, `WikiController`, `OrdersController`, `ProductsService`, `Home Data Module`?**
|
- **Why does `ApiResponse` connect `Admin Sidebar and Layout` to `Pet Management Controller`, `UsersService`, `OrdersService`, `auth.controller.ts`, `WikiController`, `ProductsService`, `Home Data Module`, `BlogsController`, `Admin Settings Managers`?**
|
||||||
_High betweenness centrality (0.039) - this node is a cross-community bridge._
|
_High betweenness centrality (0.033) - this node is a cross-community bridge._
|
||||||
- **Why does `Roles()` connect `Roles` to `ZibalService`, `JwtAuthGuard`, `ContactService`, `CmsController`, `BannersService`, `WholesaleApplyDto`, `ProductsService`, `B2B Inquiry Controller`, `Ingredient Management Controller`, `Prescription Review Controller`, `Smart Advisor Controller`, `Testimonials Management Controller`, `SettingsService`?**
|
- **Why does `Roles()` connect `Roles` to `SettingsController`, `ContactService`, `CMS Content Management`, `WholesaleApplyDto`, `ProductsService`, `B2B Inquiry Controller`, `Banner Management Controller`, `Ingredient Management Controller`, `Prescription Review Controller`, `Smart Advisor Controller`, `Testimonials Management Controller`?**
|
||||||
_High betweenness centrality (0.037) - this node is a cross-community bridge._
|
_High betweenness centrality (0.020) - this node is a cross-community bridge._
|
||||||
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `AdminService`, `pets/pets.controller.ts`, `UsersService`, `CmsController`, `OrdersService`, `CreateVideoDto`, `MediaController`, `admin.module.ts`, `SettingsService`?**
|
- **Why does `JwtAuthGuard` connect `Roles` to `AdminService`, `pagination.dto.ts`, `ZibalService`, `UsersService`, `CreateVideoDto`, `ProductsService`, `admin.module.ts`, `Pet DTOs and Controller`?**
|
||||||
_High betweenness centrality (0.022) - this node is a cross-community bridge._
|
_High betweenness centrality (0.020) - this node is a cross-community bridge._
|
||||||
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
|
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
|
||||||
_1179 weakly-connected nodes found - possible documentation gaps or missing edges._
|
_1170 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
- **Should `AdminService` be split into smaller, more focused modules?**
|
- **Should `AdminService` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.06841046277665996 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.06690140845070422 - nodes in this community are weakly interconnected._
|
||||||
- **Should `pets/pets.controller.ts` be split into smaller, more focused modules?**
|
- **Should `SettingsController` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.053923541247484906 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.1251778093883357 - nodes in this community are weakly interconnected._
|
||||||
- **Should `UsersService` be split into smaller, more focused modules?**
|
- **Should `UsersService` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.06766917293233082 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.06093189964157706 - nodes in this community are weakly interconnected._
|
||||||
@ -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