feat(backend): add wallet refund transactions and enhance SMS automation rules
This commit is contained in:
parent
e15c25dae1
commit
5e3e3958c5
@ -402,6 +402,9 @@ model Order {
|
||||
paymentMethod String? @default("card") @map("payment_method") @db.VarChar(30)
|
||||
shippingAddress String? @map("shipping_address") @db.Text
|
||||
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
||||
refundStatus String? @default("none") @map("refund_status") @db.VarChar(30) // none, wallet, gateway, partial
|
||||
refundedAmount Decimal @default(0.00) @map("refunded_amount") @db.Decimal(15, 2)
|
||||
refundMethod String? @map("refund_method") @db.VarChar(30) // wallet, zibal
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@ -204,7 +204,8 @@ export class AdminController {
|
||||
@Body('trackingCode') trackingCode?: string,
|
||||
@Body('trackingNumber') trackingNumber?: string,
|
||||
) {
|
||||
const finalTracking = trackingNumber !== undefined ? trackingNumber : trackingCode;
|
||||
const finalTracking =
|
||||
trackingNumber !== undefined ? trackingNumber : trackingCode;
|
||||
const order = await this.adminService.updateOrderStatus(
|
||||
id,
|
||||
status,
|
||||
|
||||
@ -859,22 +859,31 @@ export class AdminService {
|
||||
}
|
||||
if (!shipping.province || !shipping.address) {
|
||||
let text = trimmed;
|
||||
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
|
||||
const recipientMatch = text.match(
|
||||
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
|
||||
);
|
||||
if (recipientMatch) {
|
||||
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
|
||||
if (!shipping.fullName)
|
||||
shipping.fullName = recipientMatch[1]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2])
|
||||
shipping.phone = recipientMatch[2]?.trim();
|
||||
text = text.replace(recipientMatch[0], '').trim();
|
||||
}
|
||||
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
|
||||
if (postalMatch) {
|
||||
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
|
||||
if (!shipping.postalCode)
|
||||
shipping.postalCode = postalMatch[1]?.trim();
|
||||
text = text.replace(postalMatch[0], '').trim();
|
||||
}
|
||||
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
|
||||
const parts = text
|
||||
.split(/[،,]/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
if (!shipping.province) shipping.province = parts[0];
|
||||
if (!shipping.city) shipping.city = parts[1];
|
||||
if (!shipping.address) shipping.address = parts.slice(2).join('، ');
|
||||
if (!shipping.address)
|
||||
shipping.address = parts.slice(2).join('، ');
|
||||
} else if (parts.length === 2) {
|
||||
if (!shipping.province) shipping.province = parts[0];
|
||||
if (!shipping.address) shipping.address = parts[1];
|
||||
@ -897,25 +906,35 @@ export class AdminService {
|
||||
customerFullName
|
||||
).trim();
|
||||
|
||||
const finalTracking = trackingNumber || updatedOrder.trackingNumber || '';
|
||||
const finalTracking =
|
||||
trackingNumber || updatedOrder.trackingNumber || '';
|
||||
|
||||
const orderData = {
|
||||
orderNumber: updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8),
|
||||
orderNumber:
|
||||
updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8),
|
||||
amount: Number(updatedOrder.totalAmount || 0).toLocaleString('fa-IR'),
|
||||
customerName: customerFullName,
|
||||
customerPhone: user?.mobile || shipping.phone || '',
|
||||
recipientName: recipientFullName,
|
||||
recipientPhone: shipping.phone || user?.mobile || '',
|
||||
provinceCity: (shipping.province && shipping.city ? `${shipping.province} - ${shipping.city}` : shipping.province || shipping.city || 'ثبت نشده').trim(),
|
||||
provinceCity: (shipping.province && shipping.city
|
||||
? `${shipping.province} - ${shipping.city}`
|
||||
: shipping.province || shipping.city || 'ثبت نشده'
|
||||
).trim(),
|
||||
postalCode: shipping.postalCode || shipping.zipCode || '',
|
||||
address: (shipping.address || (typeof rawShipping === 'string' ? rawShipping : '')).trim(),
|
||||
address: (
|
||||
shipping.address ||
|
||||
(typeof rawShipping === 'string' ? rawShipping : '')
|
||||
).trim(),
|
||||
trackingCode: finalTracking || 'ارسال با پیک شهری',
|
||||
shippingTrackingCode: finalTracking || 'ارسال با پیک شهری',
|
||||
shippingMethod: finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری',
|
||||
orderDate: new Date().toLocaleDateString('fa-IR'),
|
||||
};
|
||||
|
||||
this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {});
|
||||
this.smsService
|
||||
.triggerEvent('ORDER_SHIPPED', orderData)
|
||||
.catch(() => {});
|
||||
} catch (smsErr) {
|
||||
// Silently catch SMS errors to avoid breaking status update
|
||||
}
|
||||
@ -944,6 +963,12 @@ export class AdminService {
|
||||
);
|
||||
}
|
||||
|
||||
if (order.refundStatus === 'wallet' || order.refundStatus === 'gateway') {
|
||||
throw new BadRequestException(
|
||||
'برای این سفارش قبلاً استرداد وجه انجام شده است',
|
||||
);
|
||||
}
|
||||
|
||||
const refundAmount =
|
||||
customAmount !== undefined && customAmount > 0
|
||||
? customAmount
|
||||
@ -980,6 +1005,9 @@ export class AdminService {
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'cancelled',
|
||||
refundStatus: 'wallet',
|
||||
refundedAmount: refundAmount,
|
||||
refundMethod: 'wallet',
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -1000,11 +1028,15 @@ export class AdminService {
|
||||
customerPhone: order.user.mobile,
|
||||
amount: Number(refundAmount || 0).toLocaleString('fa-IR'),
|
||||
balance: Number(user?.walletBalance || 0).toLocaleString('fa-IR'),
|
||||
reason: reason || `استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`,
|
||||
reason:
|
||||
reason ||
|
||||
`استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`,
|
||||
date: new Date().toLocaleDateString('fa-IR'),
|
||||
};
|
||||
|
||||
this.smsService.triggerEvent('WALLET_CHARGED', refundData).catch(() => {});
|
||||
this.smsService
|
||||
.triggerEvent('WALLET_CHARGED', refundData)
|
||||
.catch(() => {});
|
||||
} catch (err) {
|
||||
// Silently catch SMS errors
|
||||
}
|
||||
@ -1290,8 +1322,14 @@ export class AdminService {
|
||||
customerName: customerFullName,
|
||||
customerPhone: updatedUser.mobile,
|
||||
amount: Number(amount || 0).toLocaleString('fa-IR'),
|
||||
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'),
|
||||
reason: description || (type === 'refund' ? 'استرداد وجه به کیف پول' : 'شارژ کیف پول توسط مدیریت'),
|
||||
balance: Number(updatedUser.walletBalance || 0).toLocaleString(
|
||||
'fa-IR',
|
||||
),
|
||||
reason:
|
||||
description ||
|
||||
(type === 'refund'
|
||||
? 'استرداد وجه به کیف پول'
|
||||
: 'شارژ کیف پول توسط مدیریت'),
|
||||
date: new Date().toLocaleDateString('fa-IR'),
|
||||
};
|
||||
|
||||
|
||||
@ -304,12 +304,16 @@ export class ProductDto {
|
||||
@IsArray()
|
||||
keyHighlights?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'کارتهای شاخص «چرا این محصول؟» (۳ کارت مزایا)' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'کارتهای شاخص «چرا این محصول؟» (۳ کارت مزایا)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
keyBenefits?: Array<{ icon: string; title: string; description: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)',
|
||||
})
|
||||
@IsOptional()
|
||||
analysis?: Record<string, string>;
|
||||
|
||||
@ -327,12 +331,16 @@ export class ProductDto {
|
||||
@IsOptional()
|
||||
contraindications?: string[] | string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
expectedResults?: Array<{ icon: string; text: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)',
|
||||
})
|
||||
@IsOptional()
|
||||
specialist?: {
|
||||
name?: string;
|
||||
|
||||
@ -27,39 +27,93 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
|
||||
{
|
||||
event: 'ORDER_PAID',
|
||||
label: 'پرداخت موفق سفارش (Order Paid)',
|
||||
description: 'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول',
|
||||
description:
|
||||
'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول',
|
||||
variables: [
|
||||
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' },
|
||||
{
|
||||
key: 'orderNumber',
|
||||
label: 'شماره سفارش / فاکتور',
|
||||
sample: 'CN-140308-01',
|
||||
},
|
||||
{ key: 'amount', label: 'مبلغ پرداختی (تومان)', sample: '۱,۴۵۰,۰۰۰' },
|
||||
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
|
||||
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' },
|
||||
{
|
||||
key: 'customerPhone',
|
||||
label: 'شماره موبایل خریدار',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{ key: 'recipientName', label: 'نام تحویلگیرنده', sample: 'رضا حسینی' },
|
||||
{ key: 'recipientPhone', label: 'تلفن تماس تحویلگیرنده', sample: '09351234567' },
|
||||
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' },
|
||||
{
|
||||
key: 'recipientPhone',
|
||||
label: 'تلفن تماس تحویلگیرنده',
|
||||
sample: '09351234567',
|
||||
},
|
||||
{
|
||||
key: 'provinceCity',
|
||||
label: 'استان و شهر مقصد',
|
||||
sample: 'تهران - تهران',
|
||||
},
|
||||
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
|
||||
{ key: 'address', label: 'نشانی کامل پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' },
|
||||
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' },
|
||||
{
|
||||
key: 'address',
|
||||
label: 'نشانی کامل پستی',
|
||||
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
|
||||
},
|
||||
{
|
||||
key: 'itemsSummary',
|
||||
label: 'خلاصه اقلام سفارش',
|
||||
sample: 'شیر خشک توله سگ (۲)',
|
||||
},
|
||||
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
|
||||
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'آنلاین' },
|
||||
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
|
||||
{ key: 'refNumber', label: 'شماره پیگیری پرداخت بانکی', sample: '83920192' },
|
||||
{
|
||||
key: 'refNumber',
|
||||
label: 'شماره پیگیری پرداخت بانکی',
|
||||
sample: '83920192',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
event: 'ORDER_CREATED',
|
||||
label: 'ثبت سفارش کارت به کارت یا غیرآنلاین',
|
||||
description: 'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت میشود',
|
||||
description:
|
||||
'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت میشود',
|
||||
variables: [
|
||||
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' },
|
||||
{
|
||||
key: 'orderNumber',
|
||||
label: 'شماره سفارش / فاکتور',
|
||||
sample: 'CN-140308-01',
|
||||
},
|
||||
{ key: 'amount', label: 'مبلغ کل سفارش', sample: '۱,۴۵۰,۰۰۰' },
|
||||
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
|
||||
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' },
|
||||
{
|
||||
key: 'customerPhone',
|
||||
label: 'شماره موبایل خریدار',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{ key: 'recipientName', label: 'نام تحویلگیرنده', sample: 'رضا حسینی' },
|
||||
{ key: 'recipientPhone', label: 'تلفن تحویلگیرنده', sample: '09351234567' },
|
||||
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' },
|
||||
{
|
||||
key: 'recipientPhone',
|
||||
label: 'تلفن تحویلگیرنده',
|
||||
sample: '09351234567',
|
||||
},
|
||||
{
|
||||
key: 'provinceCity',
|
||||
label: 'استان و شهر مقصد',
|
||||
sample: 'تهران - تهران',
|
||||
},
|
||||
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
|
||||
{ key: 'address', label: 'نشانی پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' },
|
||||
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' },
|
||||
{
|
||||
key: 'address',
|
||||
label: 'نشانی پستی',
|
||||
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
|
||||
},
|
||||
{
|
||||
key: 'itemsSummary',
|
||||
label: 'خلاصه اقلام سفارش',
|
||||
sample: 'شیر خشک توله سگ (۲)',
|
||||
},
|
||||
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
|
||||
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'کارت به کارت' },
|
||||
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
|
||||
@ -71,11 +125,23 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
|
||||
description: 'هنگام تغییر وضعیت سفارش به ارسال شده یا درج کد رهگیری پستی',
|
||||
variables: [
|
||||
{ key: 'orderNumber', label: 'شماره سفارش', sample: 'CN-140308-01' },
|
||||
{ key: 'shippingTrackingCode', label: 'کد رهگیری ۲۴ رقمی پست', sample: '184920394857283948571928' },
|
||||
{ key: 'trackingCode', label: 'کد رهگیری پستی (مترادف)', sample: '184920394857283948571928' },
|
||||
{
|
||||
key: 'shippingTrackingCode',
|
||||
label: 'کد رهگیری ۲۴ رقمی پست',
|
||||
sample: '184920394857283948571928',
|
||||
},
|
||||
{
|
||||
key: 'trackingCode',
|
||||
label: 'کد رهگیری پستی (مترادف)',
|
||||
sample: '184920394857283948571928',
|
||||
},
|
||||
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
|
||||
{ key: 'recipientName', label: 'نام تحویلگیرنده', sample: 'رضا حسینی' },
|
||||
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' },
|
||||
{
|
||||
key: 'customerPhone',
|
||||
label: 'شماره همراه مشتری',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{ key: 'shippingMethod', label: 'روش ارسال', sample: 'پست پیشتاز' },
|
||||
],
|
||||
},
|
||||
@ -84,9 +150,21 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
|
||||
label: 'درخواست همکاری و عمدهفروشی B2B',
|
||||
description: 'هنگام ارسال فرم همکاری همکاران و کلینیکهای دامپزشکی',
|
||||
variables: [
|
||||
{ key: 'applicantName', label: 'نام متقاضی / فروشگاه', sample: 'پتشاپ آلفا' },
|
||||
{ key: 'applicantPhone', label: 'شماره همراه متقاضی', sample: '09123456789' },
|
||||
{ key: 'companyName', label: 'نام شرکت یا مجموعه', sample: 'فروشگاه آلفا' },
|
||||
{
|
||||
key: 'applicantName',
|
||||
label: 'نام متقاضی / فروشگاه',
|
||||
sample: 'پتشاپ آلفا',
|
||||
},
|
||||
{
|
||||
key: 'applicantPhone',
|
||||
label: 'شماره همراه متقاضی',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{
|
||||
key: 'companyName',
|
||||
label: 'نام شرکت یا مجموعه',
|
||||
sample: 'فروشگاه آلفا',
|
||||
},
|
||||
{ key: 'businessType', label: 'نوع فعالیت', sample: 'پتشاپ' },
|
||||
{ key: 'date', label: 'تاریخ ثبت درخواست', sample: '۱۴۰۳/۰۶/۱۵' },
|
||||
],
|
||||
@ -97,23 +175,44 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
|
||||
description: 'هنگامی که کاربری در فرم تماس پیامی ارسال میکند',
|
||||
variables: [
|
||||
{ key: 'senderName', label: 'نام فرستنده پیام', sample: 'سارا احمدی' },
|
||||
{ key: 'senderPhone', label: 'شماره همراه فرستنده', sample: '09123456789' },
|
||||
{
|
||||
key: 'senderPhone',
|
||||
label: 'شماره همراه فرستنده',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{ key: 'email', label: 'ایمیل فرستنده', sample: 'user@example.com' },
|
||||
{ key: 'subject', label: 'موضوع پیام', sample: 'مشاوره خرید مکمل' },
|
||||
{ key: 'message', label: 'متن پیام', sample: 'درخواست راهنمایی در مورد دوز کلسیم' },
|
||||
{
|
||||
key: 'message',
|
||||
label: 'متن پیام',
|
||||
sample: 'درخواست راهنمایی در مورد دوز کلسیم',
|
||||
},
|
||||
{ key: 'date', label: 'تاریخ ارسال', sample: '۱۴۰۳/۰۶/۱۵' },
|
||||
],
|
||||
},
|
||||
{
|
||||
event: 'WALLET_CHARGED',
|
||||
label: 'شارژ و افزایش موجودی کیف پول',
|
||||
description: 'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کشبک یا استرداد)',
|
||||
description:
|
||||
'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کشبک یا استرداد)',
|
||||
variables: [
|
||||
{ key: 'customerName', label: 'نام مشتری', sample: 'علی محمدی' },
|
||||
{ key: 'amount', label: 'مبلغ شارژ (تومان)', sample: '۵۰,۰۰۰' },
|
||||
{ key: 'balance', label: 'موجودی جدید کیف پول (تومان)', sample: '۱۵۰,۰۰۰' },
|
||||
{ key: 'reason', label: 'علت / نوع شارژ', sample: 'شارژ آنلاین / پاداش خرید (Cashback)' },
|
||||
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' },
|
||||
{
|
||||
key: 'balance',
|
||||
label: 'موجودی جدید کیف پول (تومان)',
|
||||
sample: '۱۵۰,۰۰۰',
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
label: 'علت / نوع شارژ',
|
||||
sample: 'شارژ آنلاین / پاداش خرید (Cashback)',
|
||||
},
|
||||
{
|
||||
key: 'customerPhone',
|
||||
label: 'شماره همراه مشتری',
|
||||
sample: '09123456789',
|
||||
},
|
||||
{ key: 'date', label: 'تاریخ شارژ', sample: '۱۴۰۳/۰۶/۱۵' },
|
||||
],
|
||||
},
|
||||
@ -128,7 +227,8 @@ export const DEFAULT_SMS_RULES: SmsRule[] = [
|
||||
recipientType: 'customer',
|
||||
patternId: 508081,
|
||||
variables: ['customerName', 'orderNumber', 'amount'],
|
||||
description: 'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت',
|
||||
description:
|
||||
'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت',
|
||||
},
|
||||
{
|
||||
id: 'default-order-admin',
|
||||
|
||||
@ -163,9 +163,7 @@ export class SmsService {
|
||||
|
||||
const activeRules = (config.rules || []).filter(
|
||||
(rule) =>
|
||||
rule.enabled &&
|
||||
rule.event === event &&
|
||||
Number(rule.patternId) > 0,
|
||||
rule.enabled && rule.event === event && Number(rule.patternId) > 0,
|
||||
);
|
||||
|
||||
if (activeRules.length > 0) {
|
||||
@ -297,7 +295,8 @@ export class SmsService {
|
||||
if (!config.username || !config.password) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.',
|
||||
message:
|
||||
'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.',
|
||||
};
|
||||
}
|
||||
|
||||
@ -323,7 +322,8 @@ export class SmsService {
|
||||
if (!phone) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.',
|
||||
message:
|
||||
'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.',
|
||||
};
|
||||
}
|
||||
|
||||
@ -496,7 +496,8 @@ export class SmsService {
|
||||
const patterns: MeliPayamakPattern[] = [];
|
||||
// MeliPayamak uses <ShareServiceBody> and child tags <BodyID>, <Title>, <Body>, <BodyStatus>
|
||||
// We match both <ShareServiceBody> and <SharedServiceBody> for backwards/cross compatibility
|
||||
const itemRegex = /<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi;
|
||||
const itemRegex =
|
||||
/<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = itemRegex.exec(xml)) !== null) {
|
||||
@ -504,7 +505,9 @@ export class SmsService {
|
||||
const idMatch = block.match(/<(?:BodyID|Id)>(\d+)<\/(?:BodyID|Id)>/i);
|
||||
const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
|
||||
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i);
|
||||
const statusMatch = block.match(/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i);
|
||||
const statusMatch = block.match(
|
||||
/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i,
|
||||
);
|
||||
|
||||
if (idMatch) {
|
||||
const id = parseInt(idMatch[1], 10);
|
||||
@ -616,7 +619,9 @@ export class SmsService {
|
||||
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}`;
|
||||
return args.length > 0
|
||||
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
|
||||
: `الگو ${patternId}`;
|
||||
}
|
||||
|
||||
let rendered = target.body;
|
||||
@ -626,7 +631,9 @@ export class SmsService {
|
||||
});
|
||||
return rendered;
|
||||
} catch {
|
||||
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`;
|
||||
return args.length > 0
|
||||
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
|
||||
: `الگو ${patternId}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -761,19 +768,34 @@ export class SmsService {
|
||||
}
|
||||
|
||||
// Check legacy default bindings
|
||||
if (p.id === config.otpBodyId && !assigned.some((a) => a.includes('OTP'))) {
|
||||
if (
|
||||
p.id === config.otpBodyId &&
|
||||
!assigned.some((a) => a.includes('OTP'))
|
||||
) {
|
||||
assigned.push('کد تایید OTP');
|
||||
}
|
||||
if (p.id === config.orderBodyId && !assigned.some((a) => a.includes('سفارش'))) {
|
||||
if (
|
||||
p.id === config.orderBodyId &&
|
||||
!assigned.some((a) => a.includes('سفارش'))
|
||||
) {
|
||||
assigned.push('پترن پیشفرض سفارش');
|
||||
}
|
||||
if (p.id === config.shippingBodyId && !assigned.some((a) => a.includes('پست'))) {
|
||||
if (
|
||||
p.id === config.shippingBodyId &&
|
||||
!assigned.some((a) => a.includes('پست'))
|
||||
) {
|
||||
assigned.push('پترن پیشفرض پست');
|
||||
}
|
||||
if (p.id === config.b2bBodyId && !assigned.some((a) => a.includes('B2B'))) {
|
||||
if (
|
||||
p.id === config.b2bBodyId &&
|
||||
!assigned.some((a) => a.includes('B2B'))
|
||||
) {
|
||||
assigned.push('پترن پیشفرض B2B');
|
||||
}
|
||||
if (p.id === config.petCareBodyId && !assigned.some((a) => a.includes('پت'))) {
|
||||
if (
|
||||
p.id === config.petCareBodyId &&
|
||||
!assigned.some((a) => a.includes('پت'))
|
||||
) {
|
||||
assigned.push('پترن پیشفرض پرونده پت');
|
||||
}
|
||||
|
||||
@ -1029,43 +1051,62 @@ export class SmsService {
|
||||
});
|
||||
|
||||
// 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),
|
||||
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,
|
||||
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}`,
|
||||
);
|
||||
(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,
|
||||
@ -1073,50 +1114,37 @@ export class SmsService {
|
||||
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,
|
||||
messageText: renderedText,
|
||||
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,
|
||||
messageText: renderedText,
|
||||
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();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -257,11 +257,9 @@ export class OrdersService {
|
||||
// Send Order Confirmation SMS for wallet orders
|
||||
if (user.mobile) {
|
||||
const orderData = this.buildOrderEventData(createdWalletOrder, user);
|
||||
this.smsService
|
||||
.triggerEvent('ORDER_PAID', orderData)
|
||||
.catch((err) => {
|
||||
this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
|
||||
});
|
||||
this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
|
||||
this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
return createdWalletOrder;
|
||||
@ -324,29 +322,36 @@ export class OrdersService {
|
||||
shipping = {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If not parsed as JSON or fields are missing, extract from formatted address string:
|
||||
// Pattern: "استان، شهر، آدرس پستی (کد پستی: ۱۲۳۴۵۶۷۸۹۰) (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)"
|
||||
if (!shipping.province || !shipping.address) {
|
||||
let text = trimmed;
|
||||
|
||||
// 1. Extract recipient & phone: (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)
|
||||
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
|
||||
const recipientMatch = text.match(
|
||||
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
|
||||
);
|
||||
if (recipientMatch) {
|
||||
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2])
|
||||
shipping.phone = recipientMatch[2]?.trim();
|
||||
text = text.replace(recipientMatch[0], '').trim();
|
||||
}
|
||||
|
||||
// 2. Extract postal code: (کد پستی: ۱۲۳۴۵۶۷۸۹۰)
|
||||
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
|
||||
if (postalMatch) {
|
||||
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
|
||||
if (!shipping.postalCode)
|
||||
shipping.postalCode = postalMatch[1]?.trim();
|
||||
text = text.replace(postalMatch[0], '').trim();
|
||||
}
|
||||
|
||||
// 3. Extract province and city: "استان، شهر، بقیه آدرس" or "استان - شهر - بقیه آدرس"
|
||||
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
|
||||
const parts = text
|
||||
.split(/[،,]/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
if (!shipping.province) shipping.province = parts[0];
|
||||
if (!shipping.city) shipping.city = parts[1];
|
||||
@ -362,10 +367,7 @@ export class OrdersService {
|
||||
|
||||
const items = order.orderItems || [];
|
||||
const itemsSummary = items
|
||||
.map(
|
||||
(i: any) =>
|
||||
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
|
||||
)
|
||||
.map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
|
||||
.join('، ');
|
||||
const itemCount = items.reduce(
|
||||
(sum: number, i: any) => sum + (i.quantity || 1),
|
||||
@ -384,11 +386,7 @@ export class OrdersService {
|
||||
customerFullName
|
||||
).trim();
|
||||
|
||||
const recipientMobile = (
|
||||
shipping.phone ||
|
||||
user?.mobile ||
|
||||
''
|
||||
).trim();
|
||||
const recipientMobile = (shipping.phone || user?.mobile || '').trim();
|
||||
|
||||
const provinceCity = (
|
||||
shipping.province && shipping.city
|
||||
@ -447,14 +445,17 @@ export class OrdersService {
|
||||
const mobile = updatedOrder.user?.mobile;
|
||||
const finalTracking = trackingCode || updatedOrder.trackingNumber || '';
|
||||
if (status === 'shipped' && mobile) {
|
||||
const orderData = this.buildOrderEventData(updatedOrder, updatedOrder.user);
|
||||
const orderData = this.buildOrderEventData(
|
||||
updatedOrder,
|
||||
updatedOrder.user,
|
||||
);
|
||||
orderData.trackingCode = finalTracking || 'ارسال با پیک شهری';
|
||||
orderData.shippingTrackingCode = finalTracking || 'ارسال با پیک شهری';
|
||||
orderData.shippingMethod = finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری';
|
||||
orderData.shippingMethod = finalTracking
|
||||
? 'پست پیشتاز / تیپاکس'
|
||||
: 'پیک شهری';
|
||||
|
||||
this.smsService
|
||||
.triggerEvent('ORDER_SHIPPED', orderData)
|
||||
.catch(() => {});
|
||||
this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {});
|
||||
}
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
@ -251,9 +251,10 @@ export class PaymentController {
|
||||
tryReverse?: boolean;
|
||||
cardNumber?: string;
|
||||
description?: string;
|
||||
orderId?: string;
|
||||
},
|
||||
) {
|
||||
return this.zibalService.requestRefund(body);
|
||||
return this.paymentService.handleAdminGatewayRefund(body);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
|
||||
@ -511,22 +511,31 @@ export class PaymentService {
|
||||
}
|
||||
if (!shipping.province || !shipping.address) {
|
||||
let text = trimmed;
|
||||
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
|
||||
const recipientMatch = text.match(
|
||||
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
|
||||
);
|
||||
if (recipientMatch) {
|
||||
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
|
||||
if (!shipping.fullName)
|
||||
shipping.fullName = recipientMatch[1]?.trim();
|
||||
if (!shipping.phone && recipientMatch[2])
|
||||
shipping.phone = recipientMatch[2]?.trim();
|
||||
text = text.replace(recipientMatch[0], '').trim();
|
||||
}
|
||||
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
|
||||
if (postalMatch) {
|
||||
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
|
||||
if (!shipping.postalCode)
|
||||
shipping.postalCode = postalMatch[1]?.trim();
|
||||
text = text.replace(postalMatch[0], '').trim();
|
||||
}
|
||||
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
|
||||
const parts = text
|
||||
.split(/[،,]/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
if (!shipping.province) shipping.province = parts[0];
|
||||
if (!shipping.city) shipping.city = parts[1];
|
||||
if (!shipping.address) shipping.address = parts.slice(2).join('، ');
|
||||
if (!shipping.address)
|
||||
shipping.address = parts.slice(2).join('، ');
|
||||
} else if (parts.length === 2) {
|
||||
if (!shipping.province) shipping.province = parts[0];
|
||||
if (!shipping.address) shipping.address = parts[1];
|
||||
@ -538,10 +547,7 @@ export class PaymentService {
|
||||
|
||||
const items = order?.orderItems || [];
|
||||
const itemsSummary = items
|
||||
.map(
|
||||
(i: any) =>
|
||||
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
|
||||
)
|
||||
.map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
|
||||
.join('، ');
|
||||
const itemCount = items.reduce(
|
||||
(sum: number, i: any) => sum + (i.quantity || 1),
|
||||
@ -562,11 +568,7 @@ export class PaymentService {
|
||||
customerName
|
||||
).trim();
|
||||
|
||||
const recipientMobile = (
|
||||
shipping.phone ||
|
||||
customerMobile ||
|
||||
''
|
||||
).trim();
|
||||
const recipientMobile = (shipping.phone || customerMobile || '').trim();
|
||||
|
||||
const provinceCity = (
|
||||
shipping.province && shipping.city
|
||||
@ -601,11 +603,9 @@ export class PaymentService {
|
||||
orderDate: new Date().toLocaleDateString('fa-IR'),
|
||||
};
|
||||
|
||||
this.smsService
|
||||
.triggerEvent('ORDER_PAID', orderData)
|
||||
.catch((err) => {
|
||||
this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
|
||||
});
|
||||
this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
|
||||
this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Send SMS notification if wallet top-up
|
||||
@ -632,7 +632,9 @@ export class PaymentService {
|
||||
this.smsService
|
||||
.triggerEvent('WALLET_CHARGED', topupData)
|
||||
.catch((err) => {
|
||||
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`);
|
||||
this.logger.warn(
|
||||
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -1130,7 +1132,9 @@ export class PaymentService {
|
||||
this.smsService
|
||||
.triggerEvent('WALLET_CHARGED', topupData)
|
||||
.catch((err) => {
|
||||
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`);
|
||||
this.logger.warn(
|
||||
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -1176,4 +1180,99 @@ export class PaymentService {
|
||||
async checkGatewayHealth() {
|
||||
return this.zibalService.checkHealth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 12. Admin: Process Gateway Refund (Zibal) with Wallet Balance Deduction if previously refunded to wallet
|
||||
*/
|
||||
async handleAdminGatewayRefund(options: {
|
||||
trackId: string | number;
|
||||
amount?: number; // Rial
|
||||
tryReverse?: boolean;
|
||||
cardNumber?: string;
|
||||
description?: string;
|
||||
orderId?: string;
|
||||
}) {
|
||||
// 1. If orderId is not passed, attempt to find by trackId
|
||||
let order = options.orderId
|
||||
? await this.prisma.order.findUnique({
|
||||
where: { id: options.orderId },
|
||||
include: { user: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!order && options.trackId) {
|
||||
const tx = await this.prisma.paymentTransaction.findUnique({
|
||||
where: { trackId: String(options.trackId) },
|
||||
include: { order: { include: { user: true } } },
|
||||
});
|
||||
if (tx?.order) {
|
||||
order = tx.order;
|
||||
}
|
||||
}
|
||||
|
||||
if (order && order.refundStatus === 'gateway') {
|
||||
throw new BadRequestException(
|
||||
'برای این سفارش قبلاً استرداد درگاه بانکی انجام شده است',
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Execute refund via Zibal
|
||||
const zibalRes = await this.zibalService.requestRefund({
|
||||
trackId: options.trackId,
|
||||
amount: options.amount,
|
||||
tryReverse: options.tryReverse,
|
||||
cardNumber: options.cardNumber,
|
||||
description: options.description,
|
||||
});
|
||||
|
||||
const isSuccess =
|
||||
zibalRes?.result === 1 ||
|
||||
zibalRes?.status === 1 ||
|
||||
zibalRes?.result === 100 ||
|
||||
zibalRes?.success === true;
|
||||
|
||||
if (isSuccess && order) {
|
||||
const refundAmountTomans = options.amount
|
||||
? Math.round(Number(options.amount) / 10)
|
||||
: Number(order.totalAmount || 0);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// If order was previously refunded to wallet, deduct that amount from user's wallet!
|
||||
if (order.refundStatus === 'wallet' && order.userId) {
|
||||
const deductionAmount = Number(
|
||||
order.refundedAmount || refundAmountTomans,
|
||||
);
|
||||
await tx.user.update({
|
||||
where: { id: order.userId },
|
||||
data: {
|
||||
walletBalance: { decrement: deductionAmount },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.walletTransaction.create({
|
||||
data: {
|
||||
userId: order.userId,
|
||||
amount: deductionAmount,
|
||||
type: 'withdraw',
|
||||
status: 'completed',
|
||||
description: `کسر موجودی کیف پول به علت استرداد بانکی سفارش #${order.trackingNumber || order.id.slice(0, 8)} از طریق زیبال`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Update order refund status to gateway
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'cancelled',
|
||||
refundStatus: 'gateway',
|
||||
refundedAmount: refundAmountTomans,
|
||||
refundMethod: 'zibal',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return zibalRes;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +223,10 @@ export class SettingsController {
|
||||
@Roles('Admin')
|
||||
@ApiBearerAuth()
|
||||
@Get('sms/events')
|
||||
@ApiOperation({ summary: 'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترنها' })
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترنها',
|
||||
})
|
||||
getSmsEvents() {
|
||||
return this.settingsService.getSmsEvents();
|
||||
}
|
||||
@ -232,11 +235,10 @@ export class SettingsController {
|
||||
@Roles('Admin')
|
||||
@ApiBearerAuth()
|
||||
@Post('sms/test-rule')
|
||||
@ApiOperation({ summary: 'تست ارسال پیامک برای یک سناریوی تعریفشده با دیتای تستی' })
|
||||
testSmsRule(
|
||||
@Body('rule') rule: any,
|
||||
@Body('testPhone') testPhone?: string,
|
||||
) {
|
||||
@ApiOperation({
|
||||
summary: 'تست ارسال پیامک برای یک سناریوی تعریفشده با دیتای تستی',
|
||||
})
|
||||
testSmsRule(@Body('rule') rule: any, @Body('testPhone') testPhone?: string) {
|
||||
return this.settingsService.testSmsRule(rule, testPhone);
|
||||
}
|
||||
|
||||
|
||||
@ -608,8 +608,12 @@ export class SettingsService implements OnModuleInit {
|
||||
updates['ogImageUrl'] = String(obj.ogImageUrl);
|
||||
}
|
||||
if ('googleSiteVerification' in obj) {
|
||||
updates['googleSiteVerification'] = String(obj.googleSiteVerification ?? '');
|
||||
updates['google_site_verification'] = String(obj.googleSiteVerification ?? '');
|
||||
updates['googleSiteVerification'] = String(
|
||||
obj.googleSiteVerification ?? '',
|
||||
);
|
||||
updates['google_site_verification'] = String(
|
||||
obj.googleSiteVerification ?? '',
|
||||
);
|
||||
}
|
||||
if ('googleAnalyticsId' in obj) {
|
||||
updates['googleAnalyticsId'] = String(obj.googleAnalyticsId ?? '');
|
||||
@ -620,10 +624,14 @@ export class SettingsService implements OnModuleInit {
|
||||
updates['gtm_id'] = String(obj.googleTagManagerId ?? '');
|
||||
}
|
||||
if ('enableGoogleAnalytics' in obj) {
|
||||
updates['enableGoogleAnalytics'] = String(Boolean(obj.enableGoogleAnalytics));
|
||||
updates['enableGoogleAnalytics'] = String(
|
||||
Boolean(obj.enableGoogleAnalytics),
|
||||
);
|
||||
}
|
||||
if ('enableGoogleTagManager' in obj) {
|
||||
updates['enableGoogleTagManager'] = String(Boolean(obj.enableGoogleTagManager));
|
||||
updates['enableGoogleTagManager'] = String(
|
||||
Boolean(obj.enableGoogleTagManager),
|
||||
);
|
||||
}
|
||||
if ('clarityProjectId' in obj) {
|
||||
updates['clarityProjectId'] = String(obj.clarityProjectId ?? '');
|
||||
@ -659,7 +667,7 @@ export class SettingsService implements OnModuleInit {
|
||||
return this.smsService.testSms(targetPhone, patternId, args);
|
||||
}
|
||||
|
||||
async getSmsEvents() {
|
||||
getSmsEvents() {
|
||||
return this.smsService.getEventDefinitions();
|
||||
}
|
||||
|
||||
|
||||
@ -309,12 +309,16 @@ export class UsersService {
|
||||
customerName: customerFullName,
|
||||
customerPhone: updatedUser.mobile,
|
||||
amount: Number(amount || 0).toLocaleString('fa-IR'),
|
||||
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'),
|
||||
balance: Number(updatedUser.walletBalance || 0).toLocaleString(
|
||||
'fa-IR',
|
||||
),
|
||||
reason: 'افزایش موجودی کیف پول',
|
||||
date: new Date().toLocaleDateString('fa-IR'),
|
||||
};
|
||||
|
||||
this.smsService.triggerEvent('WALLET_CHARGED', topupData).catch(() => {});
|
||||
this.smsService
|
||||
.triggerEvent('WALLET_CHARGED', topupData)
|
||||
.catch(() => {});
|
||||
} catch (err) {
|
||||
// Silently catch SMS error
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user