From 09c85884015e1c33589c530234773c93b542e847 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sat, 5 Sep 2026 09:38:51 +0330 Subject: [PATCH] feat(sms): display full rendered SMS body in logs, streamline pattern triggers tab and remove legacy assignment dropdown --- backend/src/common/services/sms.service.ts | 467 ++++++++++-------- .../admin-panel/src/pages/SmsSettingsPage.tsx | 164 +++--- 2 files changed, 360 insertions(+), 271 deletions(-) diff --git a/backend/src/common/services/sms.service.ts b/backend/src/common/services/sms.service.ts index 25bff53..6522449 100644 --- a/backend/src/common/services/sms.service.ts +++ b/backend/src/common/services/sms.service.ts @@ -601,6 +601,35 @@ export class SmsService { }); } + /** + * Render interpolated message text using pattern body template and arguments + */ + async renderPatternMessage( + patternId?: number | null, + args: string[] = [], + ): Promise { + if (!patternId) { + return args.join(' '); + } + + try { + const patterns = await this.getPatterns(); + const target = patterns.find((p) => p.id === patternId); + if (!target || !target.body) { + return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`; + } + + let rendered = target.body; + args.forEach((val, idx) => { + const regex = new RegExp(`\\{${idx}\\}`, 'g'); + rendered = rendered.replace(regex, val || ''); + }); + return rendered; + } catch { + return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`; + } + } + /** * Fetch all registered patterns from MeliPayamak (with local cache sync) */ @@ -639,90 +668,90 @@ export class SmsService { } } - // Load local stored patterns from settings table (fallback if offline or newly added locally) + // Load cached patterns from settings table const storedSetting = await this.prisma.setting.findFirst({ - where: { category: 'sms_patterns' }, + where: { + category: 'sms_patterns', + key: { in: ['sms_patterns_cache', 'sms_patterns_config'] }, + }, + orderBy: { updatedAt: 'desc' }, }); - const localPatterns: MeliPayamakPattern[] = Array.isArray( + const cachedPatterns: MeliPayamakPattern[] = Array.isArray( storedSetting?.value, ) ? (storedSetting.value as unknown as MeliPayamakPattern[]) : []; - // Merge remote and local (remote takes precedence on ID match) + // Prioritize remote patterns, then cached patterns, then fallback defaults only if completely empty const map = new Map(); - // 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} به مبلغ {2} با موفقیت ثبت شد.\nکنینا ایران', - 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], - }); + // 1. If we have remote patterns, use them + for (const rp of remotePatterns) { + if (rp && rp.id) { + map.set(rp.id, { ...rp }); } } - for (const lp of localPatterns) { - if (lp && lp.id) map.set(lp.id, { ...lp }); + // 2. If remote returned empty or offline, use cached patterns + if (map.size === 0) { + for (const cp of cachedPatterns) { + if (cp && cp.id) { + map.set(cp.id, { ...cp }); + } + } } - for (const rp of remotePatterns) { - const existing = map.get(rp.id); - map.set(rp.id, { - ...rp, - assignedTo: existing?.assignedTo || [], - }); + // 3. Only if map is still completely empty (brand new install / unconfigured account), provide fallback defaults + if (map.size === 0) { + const defaultConfigs: Array<{ + id: number; + title: string; + body: string; + }> = [ + { + id: config.otpBodyId, + title: 'کد تایید ورود و ثبت‌نام (OTP)', + body: 'کد ورود به کنینا: {0}', + }, + { + id: config.orderBodyId, + title: 'تایید و ثبت فاکتور سفارش', + body: '{0} عزیز ، سفارش شما به شماره {1} به مبلغ {2} با موفقیت ثبت شد.\nکنینا ایران', + }, + { + id: config.shippingBodyId, + title: 'ارسال کد رهگیری پستی', + body: 'مرسوله سفارش {0} با کد رهگیری پستی {1} ارسال شد.', + }, + { + id: config.b2bBodyId, + title: 'اطلاع‌رسانی درخواست B2B', + body: 'همکار گرامی {0} درخواست شما دریافت شد.', + }, + ]; + + 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: [], + }); + } + } } - // Mark current bindings (both legacy bodyIds and dynamic scenario rules) + // Bind current scenario triggers and system assignments 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('پترن پیش‌فرض یادآور پت'); - // Check dynamic scenario rules + // Check dynamic scenario rules (Primary Assignment) if (Array.isArray(config.rules)) { for (const r of config.rules) { - if (Number(r.patternId) === p.id) { + if (Number(r.patternId) === p.id && r.enabled) { const ruleTag = `سناریو: ${r.title}`; if (!assigned.includes(ruleTag)) { assigned.push(ruleTag); @@ -731,6 +760,23 @@ export class SmsService { } } + // Check legacy default bindings + if (p.id === config.otpBodyId && !assigned.some((a) => a.includes('OTP'))) { + assigned.push('کد تایید OTP'); + } + if (p.id === config.orderBodyId && !assigned.some((a) => a.includes('سفارش'))) { + assigned.push('پترن پیش‌فرض سفارش'); + } + if (p.id === config.shippingBodyId && !assigned.some((a) => a.includes('پست'))) { + assigned.push('پترن پیش‌فرض پست'); + } + if (p.id === config.b2bBodyId && !assigned.some((a) => a.includes('B2B'))) { + assigned.push('پترن پیش‌فرض B2B'); + } + if (p.id === config.petCareBodyId && !assigned.some((a) => a.includes('پت'))) { + assigned.push('پترن پیش‌فرض پرونده پت'); + } + return { ...p, assignedTo: assigned }; }); @@ -982,88 +1028,95 @@ export class SmsService { bodyId: options.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), + // Pre-render message text for logging + void this.renderPatternMessage(options.bodyId, options.args).then((renderedText) => { + 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)) { - this.logger.log( - `[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`, - ); - await this.recordLog({ - receptor: options.to, - type: msgType, - patternId: options.bodyId, - args: options.args, - status: 'SUCCESS', - recId: String(val), - }); - resolve(true); - } else { - const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`; - this.logger.error( - `[SMS Error] Failed sending to ${options.to}. Code: ${val}`, - ); + (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)) { + this.logger.log( + `[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`, + ); + await this.recordLog({ + receptor: options.to, + type: msgType, + patternId: options.bodyId, + args: options.args, + messageText: renderedText, + status: 'SUCCESS', + recId: String(val), + }); + resolve(true); + } else { + const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`; + this.logger.error( + `[SMS Error] Failed sending to ${options.to}. Code: ${val}`, + ); + await this.recordLog({ + receptor: options.to, + type: msgType, + patternId: options.bodyId, + args: options.args, + messageText: renderedText, + status: 'FAILED', + recId: String(val), + errorMessage: errMsg, + }); + resolve(false); + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); await this.recordLog({ receptor: options.to, type: msgType, patternId: options.bodyId, args: options.args, + messageText: renderedText, status: 'FAILED', - recId: String(val), - errorMessage: errMsg, + errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`, }); resolve(false); } - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - await this.recordLog({ - receptor: options.to, - type: msgType, - patternId: options.bodyId, - args: options.args, - status: 'FAILED', - errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`, - }); - resolve(false); - } - })(); - }); - }, - ); + })(); + }); + }, + ); - req.on('error', (err: Error) => { - void (async () => { - this.logger.error( - `[SMS Exception] MeliPayamak HTTP Error: ${err.message}`, - ); - await this.recordLog({ - receptor: options.to, - type: msgType, - patternId: options.bodyId, - args: options.args, - status: 'FAILED', - errorMessage: `خطای شبکه: ${err.message}`, - }); - resolve(false); - })(); + req.on('error', (err: Error) => { + void (async () => { + this.logger.error( + `[SMS Exception] MeliPayamak HTTP Error: ${err.message}`, + ); + await this.recordLog({ + receptor: options.to, + type: msgType, + patternId: options.bodyId, + args: options.args, + messageText: renderedText, + status: 'FAILED', + errorMessage: `خطای شبکه: ${err.message}`, + }); + resolve(false); + })(); + }); + + req.write(payload); + req.end(); }); - - req.write(payload); - req.end(); }); } @@ -1106,89 +1159,95 @@ export class SmsService { 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), + void this.renderPatternMessage(bodyId, args).then((renderedText) => { + 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}`; + (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, + messageText: renderedText, + 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, + messageText: renderedText, + 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, + messageText: renderedText, status: 'FAILED', - recId: String(val), errorMessage: errMsg, }); - resolve({ - success: false, - message: errMsg, - rawResponse: json, - }); + resolve({ success: false, message: errMsg }); } - } 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.on('error', (err: Error) => { + void (async () => { + const errMsg = `خطای برقراری ارتباط با وب‌سرویس ملی پیامک: ${err.message}`; + await this.recordLog({ + receptor: targetPhone, + type: 'TEST', + patternId: bodyId, + args, + messageText: renderedText, + status: 'FAILED', + errorMessage: errMsg, + }); + resolve({ success: false, message: errMsg }); + })(); + }); + + req.write(payload); + req.end(); }); - - req.write(payload); - req.end(); }); } diff --git a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx index eee3a0b..35b3c0b 100644 --- a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx +++ b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx @@ -122,7 +122,7 @@ const toPersianDigits = (n: string | number | null | undefined): string => { }; export default function SmsSettingsPage() { - const [activeTab, setActiveTab] = useState<'settings' | 'triggers' | 'patterns' | 'logs'>('triggers'); + const [activeTab, setActiveTab] = useState<'settings' | 'patterns' | 'triggers' | 'logs'>('patterns'); // Config State const [config, setConfig] = useState({ @@ -729,24 +729,6 @@ export default function SmsSettingsPage() { پیکربندی حساب - - + + - -