Let admins skip SMS on user, class, and invoice actions, and let users change their own password with the current one.
189 lines
5.8 KiB
JavaScript
189 lines
5.8 KiB
JavaScript
// /components/payments/paymentService.js
|
|
'use strict';
|
|
|
|
const Payment = require('./paymentModel');
|
|
const AppError = require('../../utils/AppError');
|
|
const eventEmitter = require('../../events/eventEmitter');
|
|
const EVENT_NAMES = require('../../constants/eventNames');
|
|
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
|
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 { pickNotifyFlags, omitNotifyFields } = require('../../utils/notifyFlags');
|
|
const logger = require('../../utils/logger');
|
|
|
|
const getAllPayments = async (query) => {
|
|
const page = parseInt(query.page) || 1;
|
|
const limit = Math.min(parseInt(query.limit) || 20, 200);
|
|
const skip = (page - 1) * limit;
|
|
|
|
const filter = {};
|
|
if (query.userId) filter.user = query.userId;
|
|
if (query.status) filter.status = query.status;
|
|
|
|
const searchTerm = getSearchTerm(query);
|
|
if (searchTerm) {
|
|
const searchRegex = new RegExp(escapeRegex(searchTerm), 'i');
|
|
const matchedUsers = await User.find({
|
|
$or: [
|
|
{ name: searchRegex },
|
|
{ phoneNumber: searchRegex },
|
|
{ nationalIdCode: searchRegex }
|
|
]
|
|
}).select('_id').lean();
|
|
|
|
filter.$or = [
|
|
{ notes: searchRegex },
|
|
{ status: searchRegex },
|
|
{ user: { $in: matchedUsers.map((u) => u._id) } }
|
|
];
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
Payment.find(filter)
|
|
.populate({ path: 'user', select: 'name' })
|
|
.populate({ path: 'classes', select: 'name' })
|
|
.populate({ path: 'course', select: 'title' })
|
|
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
|
Payment.countDocuments(filter)
|
|
]);
|
|
|
|
return { data: items, meta: calculateMeta(total, page, limit) };
|
|
};
|
|
|
|
const getPaymentById = async (id) => {
|
|
const payment = await Payment.findById(id)
|
|
.populate({ path: 'user', select: 'name phoneNumber' })
|
|
.populate({ path: 'classes', select: 'name tuitionFee' })
|
|
.populate({ path: 'course', select: 'title price' })
|
|
.lean();
|
|
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
|
return payment;
|
|
};
|
|
|
|
const createPayment = async (body, actorId = null) => {
|
|
const notify = pickNotifyFlags(body);
|
|
const payload = omitNotifyFields(body);
|
|
const payment = await Payment.create({
|
|
...payload,
|
|
paidAmount: payload.paidAmount || 0
|
|
});
|
|
|
|
if (actorId) {
|
|
eventEmitter.emit(EVENT_NAMES.PAYMENT_CREATED, {
|
|
paymentId: payment._id,
|
|
userId: payment.user,
|
|
actorId
|
|
});
|
|
}
|
|
|
|
if (notify.sms) {
|
|
try {
|
|
const user = await User.findById(payment.user).select('name phoneNumber').lean();
|
|
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: payment.amount,
|
|
course: courseName || '-',
|
|
...schedule
|
|
}, user._id);
|
|
}
|
|
} catch (err) {
|
|
logger.error(`[createPayment] Invoice SMS failed for payment ${payment._id}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
return getPaymentById(payment._id);
|
|
};
|
|
|
|
const updatePayment = async (id, body, actorId = null) => {
|
|
const payment = await Payment.findById(id);
|
|
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
|
|
|
const previousStatus = payment.status;
|
|
Object.assign(payment, body);
|
|
await payment.save();
|
|
|
|
if (body.status && body.status !== previousStatus) {
|
|
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
|
paymentId: payment._id,
|
|
userId: payment.user,
|
|
oldStatus: previousStatus,
|
|
newStatus: payment.status,
|
|
actorId
|
|
});
|
|
}
|
|
|
|
return getPaymentById(payment._id);
|
|
};
|
|
|
|
const deletePayment = async (id) => {
|
|
const payment = await Payment.findByIdAndDelete(id);
|
|
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
|
return null;
|
|
};
|
|
|
|
const searchPayments = async (query) => getAllPayments(query);
|
|
|
|
const addTransaction = async (paymentId, trxData, actorId = null) => {
|
|
const payment = await Payment.findById(paymentId);
|
|
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
|
|
|
payment.transactions.push({
|
|
...trxData,
|
|
recordedBy: actorId || trxData.recordedBy,
|
|
date: trxData.date || new Date()
|
|
});
|
|
payment.paidAmount = payment.transactions.reduce((sum, t) => sum + (t.amount || 0), 0);
|
|
await payment.save();
|
|
return getPaymentById(paymentId);
|
|
};
|
|
|
|
const getMyPayments = async (userId, query = {}) => {
|
|
return getAllPayments({ ...query, userId });
|
|
};
|
|
|
|
module.exports = {
|
|
getAllPayments,
|
|
getPaymentById,
|
|
createPayment,
|
|
updatePayment,
|
|
deletePayment,
|
|
searchPayments,
|
|
addTransaction,
|
|
getMyPayments,
|
|
// Aliases for older call sites
|
|
getAll: getAllPayments,
|
|
getOne: getPaymentById,
|
|
create: createPayment,
|
|
recordTransaction: addTransaction,
|
|
remove: deletePayment
|
|
};
|