// /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 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('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 };