// /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 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 }); } 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 };