Files
gameno-api/components/payments/paymentService.js
T
kavehhn 3160f07873 feat: send account and invoice SMS from stored template IDs
Move sms.ir template IDs out of env into settings so SuperAdmin can manage them, and record every outbound SMS status in notifications.
2026-08-15 18:37:44 +03:30

151 lines
4.3 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 { sendInvoiceCreatedSms } = require('../../utils/senders/smsMessages');
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 payment = await Payment.create({
...body,
paidAmount: body.paidAmount || 0
});
if (actorId) {
eventEmitter.emit(EVENT_NAMES.PAYMENT_CREATED, {
paymentId: payment._id,
userId: payment.user,
actorId
});
}
try {
const user = await User.findById(payment.user).select('phoneNumber').lean();
if (user?.phoneNumber) {
await sendInvoiceCreatedSms(user.phoneNumber, payment.amount, 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
};