Add sessionHolding template, unique 8-digit codes, reason slot for sessionCancelled, and disable unused notifications
This commit is contained in:
@@ -11,10 +11,14 @@ const User = require('../users/userModel');
|
||||
const Course = require('../courses/courseModel');
|
||||
const Class = require('../classes/classModel');
|
||||
const Session = require('../sessions/sessionModel');
|
||||
const { sendInvoiceCreatedSms } = require('../../utils/senders/smsMessages');
|
||||
const { buildClassScheduleContext } = require('../../utils/classSchedule');
|
||||
const { parseImportDate } = require('../../utils/jalaliDate');
|
||||
const { pickNotifyFlags, omitNotifyFields } = require('../../utils/notifyFlags');
|
||||
const { omitNotifyFields } = require('../../utils/notifyFlags');
|
||||
const { resolveNotifyFlags } = require('../../utils/notifyResolver');
|
||||
const { notifyAction } = require('../../utils/actionNotify');
|
||||
const {
|
||||
sendInvoiceCreatedSms,
|
||||
sendPaymentStatusChangedSms,
|
||||
sendTransactionRecordedSms
|
||||
} = require('../../utils/senders/smsMessages');
|
||||
const {
|
||||
getPayableAmount,
|
||||
normalizeDiscount,
|
||||
@@ -28,6 +32,98 @@ const logger = require('../../utils/logger');
|
||||
|
||||
const PAYMENT_METHODS = new Set(['online', 'card', 'cash']);
|
||||
|
||||
const PAYMENT_STATUS_LABELS = {
|
||||
pending: 'در انتظار پرداخت',
|
||||
partial: 'پرداخت جزئی',
|
||||
paid: 'پرداختشده',
|
||||
overdue: 'معوق'
|
||||
};
|
||||
|
||||
const formatPaymentContext = async (payment) => {
|
||||
const user = await User.findById(payment.user).select('name phoneNumber email').lean();
|
||||
let courseName = '';
|
||||
let schedule = { classStartDate: '', classDays: '', courseTime: '' };
|
||||
|
||||
if (payment.course) {
|
||||
const course = await Course.findById(payment.course).select('title').lean();
|
||||
if (course?.title) courseName = course.title;
|
||||
}
|
||||
if (payment.classes?.length) {
|
||||
const cls = await Class.findById(payment.classes[0]).populate('course', 'title').lean();
|
||||
if (!courseName) {
|
||||
courseName = cls?.course?.title || cls?.name || '';
|
||||
}
|
||||
if (cls) {
|
||||
const sessions = await Session.find({ class: cls._id }).select('day startTime endTime').sort({ day: 1 }).lean();
|
||||
schedule = buildClassScheduleContext(cls, sessions);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
courseName: courseName || '-',
|
||||
schedule,
|
||||
invoiceCode: payment.uniqueCode || ''
|
||||
};
|
||||
};
|
||||
|
||||
const notifyPaymentStatusChanged = async (payment, newStatus, oldStatus) => {
|
||||
try {
|
||||
const notify = await resolveNotifyFlags({}, 'paymentStatusChanged');
|
||||
if (!notify.sms && !notify.email && !notify.bot) return;
|
||||
|
||||
const { user, courseName, invoiceCode } = await formatPaymentContext(payment);
|
||||
if (!user?.phoneNumber) return;
|
||||
|
||||
await notifyAction({
|
||||
actionKey: 'paymentStatusChanged',
|
||||
userId: user._id,
|
||||
phoneNumber: user.phoneNumber,
|
||||
email: user.email,
|
||||
subject: 'تغییر وضعیت پرداخت',
|
||||
body: `وضعیت صورتحساب ${invoiceCode} از «${PAYMENT_STATUS_LABELS[oldStatus] || oldStatus}» به «${PAYMENT_STATUS_LABELS[newStatus] || newStatus}» تغییر کرد.`,
|
||||
smsHandler: () => sendPaymentStatusChangedSms(user.phoneNumber, {
|
||||
fullName: user.name || '',
|
||||
status: newStatus,
|
||||
statusLabel: PAYMENT_STATUS_LABELS[newStatus] || newStatus,
|
||||
amount: getPayableAmount(payment),
|
||||
invoiceCode,
|
||||
course: courseName
|
||||
}, user._id)
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`[notifyPaymentStatusChanged] Failed for payment ${payment._id}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const notifyTransactionRecorded = async (payment, transaction) => {
|
||||
try {
|
||||
const notify = await resolveNotifyFlags({}, 'transactionRecorded');
|
||||
if (!notify.sms && !notify.email && !notify.bot) return;
|
||||
|
||||
const { user, invoiceCode } = await formatPaymentContext(payment);
|
||||
if (!user?.phoneNumber) return;
|
||||
|
||||
await notifyAction({
|
||||
actionKey: 'transactionRecorded',
|
||||
userId: user._id,
|
||||
phoneNumber: user.phoneNumber,
|
||||
email: user.email,
|
||||
subject: 'ثبت تراکنش',
|
||||
body: `تراکنش ${transaction.uniqueCode || ''} به مبلغ ${transaction.amount} تومان ثبت شد.`,
|
||||
smsHandler: () => sendTransactionRecordedSms(user.phoneNumber, {
|
||||
fullName: user.name || '',
|
||||
amount: transaction.amount,
|
||||
invoiceCode,
|
||||
transactionCode: transaction.uniqueCode || '',
|
||||
receiptNumber: transaction.receiptNumber || ''
|
||||
}, user._id)
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`[notifyTransactionRecorded] Failed for transaction ${transaction._id}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const populatePayment = (query) => query
|
||||
.populate({ path: 'user', select: 'name phoneNumber' })
|
||||
.populate({ path: 'classes', select: 'name tuitionFee' })
|
||||
@@ -65,7 +161,7 @@ const refreshPaymentTotals = async (payment) => {
|
||||
return payment;
|
||||
};
|
||||
|
||||
const emitPaymentStatusChangedIfNeeded = (payment, previousStatus, actorId = null) => {
|
||||
const emitPaymentStatusChangedIfNeeded = async (payment, previousStatus, actorId = null) => {
|
||||
if (payment.status === previousStatus) return;
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
||||
paymentId: payment._id,
|
||||
@@ -74,6 +170,7 @@ const emitPaymentStatusChangedIfNeeded = (payment, previousStatus, actorId = nul
|
||||
newStatus: payment.status,
|
||||
actorId
|
||||
});
|
||||
await notifyPaymentStatusChanged(payment, payment.status, previousStatus);
|
||||
};
|
||||
|
||||
const reconcilePendingTransactions = async (payment, { adjustAmount = true } = {}) => {
|
||||
@@ -197,7 +294,7 @@ const getPaymentById = async (id) => {
|
||||
};
|
||||
|
||||
const createPayment = async (body, actorId = null) => {
|
||||
const notify = pickNotifyFlags(body);
|
||||
const notify = await resolveNotifyFlags(body, 'invoiceCreated');
|
||||
const payload = omitNotifyFields(body);
|
||||
const incomingTransactions = Array.isArray(payload.transactions) ? payload.transactions : null;
|
||||
delete payload.transactions;
|
||||
@@ -229,43 +326,29 @@ const createPayment = async (body, actorId = null) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (notify.sms) {
|
||||
if (notify.sms || notify.email || notify.bot) {
|
||||
try {
|
||||
const user = await User.findById(payment.user).select('name phoneNumber').lean();
|
||||
const { user, courseName, schedule, invoiceCode } = await formatPaymentContext(payment);
|
||||
if (user?.phoneNumber) {
|
||||
let courseName = '';
|
||||
let schedule = { classStartDate: '', classDays: '', courseTime: '' };
|
||||
if (payment.course) {
|
||||
const course = await Course.findById(payment.course).select('title').lean();
|
||||
if (course?.title) courseName = course.title;
|
||||
}
|
||||
if (payment.classes && payment.classes.length > 0) {
|
||||
const cls = await Class.findById(payment.classes[0]).populate('course', 'title').lean();
|
||||
if (!courseName) {
|
||||
if (cls?.course?.title) {
|
||||
courseName = cls.course.title;
|
||||
} else if (cls?.name) {
|
||||
courseName = cls.name;
|
||||
}
|
||||
}
|
||||
if (cls) {
|
||||
const sessions = await Session.find({ class: cls._id })
|
||||
.select('day startTime endTime')
|
||||
.sort({ day: 1 })
|
||||
.lean();
|
||||
schedule = buildClassScheduleContext(cls, sessions);
|
||||
}
|
||||
}
|
||||
|
||||
await sendInvoiceCreatedSms(user.phoneNumber, {
|
||||
fullName: user.name || '',
|
||||
amount: getPayableAmount(payment),
|
||||
course: courseName || '-',
|
||||
...schedule
|
||||
}, user._id);
|
||||
await notifyAction({
|
||||
actionKey: 'invoiceCreated',
|
||||
userId: user._id,
|
||||
phoneNumber: user.phoneNumber,
|
||||
email: user.email,
|
||||
subject: 'ایجاد صورتحساب',
|
||||
body: `صورتحساب ${invoiceCode} به مبلغ ${getPayableAmount(payment)} تومان بابت «${courseName}» ایجاد شد.`,
|
||||
smsHandler: () => sendInvoiceCreatedSms(user.phoneNumber, {
|
||||
fullName: user.name || '',
|
||||
amount: getPayableAmount(payment),
|
||||
course: courseName,
|
||||
invoiceCode,
|
||||
...schedule
|
||||
}, user._id),
|
||||
requestSource: body
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[createPayment] Invoice SMS failed for payment ${payment._id}: ${err.message}`);
|
||||
logger.error(`[createPayment] Invoice notification failed for payment ${payment._id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +379,7 @@ const updatePayment = async (id, body, actorId = null) => {
|
||||
await refreshPaymentTotals(payment);
|
||||
}
|
||||
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
await emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(payment._id);
|
||||
};
|
||||
@@ -317,7 +400,7 @@ const addTransaction = async (paymentId, trxData, actorId = null) => {
|
||||
const previousStatus = payment.status;
|
||||
const payload = buildTransactionPayload(payment, { ...trxData, status: trxData.status || 'paid' }, actorId);
|
||||
if (!payload.amount) throw new AppError('VALIDATION_FAILED', { amount: 'Amount is required' }, 'مبلغ تراکنش الزامی است.');
|
||||
await Transaction.create(payload);
|
||||
const transaction = await Transaction.create(payload);
|
||||
|
||||
await reconcilePendingTransactions(payment);
|
||||
await refreshPaymentTotals(payment);
|
||||
@@ -327,7 +410,8 @@ const addTransaction = async (paymentId, trxData, actorId = null) => {
|
||||
userId: payment.user,
|
||||
actorId
|
||||
});
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
await notifyTransactionRecorded(payment, transaction);
|
||||
await emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(paymentId);
|
||||
};
|
||||
@@ -373,7 +457,7 @@ const updateTransaction = async (transactionId, body, actorId = null) => {
|
||||
// Preserve admin-edited amounts; only reconcile balance when recording new payments.
|
||||
await reconcilePendingTransactions(payment, { adjustAmount: false });
|
||||
await refreshPaymentTotals(payment);
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
await emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(payment._id);
|
||||
};
|
||||
@@ -394,7 +478,7 @@ const cancelTransaction = async (transactionId, actorId = null) => {
|
||||
await trx.save();
|
||||
|
||||
await refreshPaymentTotals(payment);
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
await emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(payment._id);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user