feat(payments): add bulk class payments creation and duplicate check

This commit is contained in:
2026-08-23 13:28:06 +03:30
parent 4c07ca2ae1
commit 28d401fd0e
5 changed files with 177 additions and 1 deletions
+130
View File
@@ -258,6 +258,13 @@ const getAllPayments = async (query) => {
}
if (query.userId) filter.user = query.userId;
if (query.status) filter.status = query.status;
if (query.classId) filter.classes = query.classId;
if (query.classes) {
const classList = Array.isArray(query.classes)
? query.classes
: query.classes.split(',').map((s) => s.trim()).filter(Boolean);
if (classList.length) filter.classes = { $in: classList };
}
const searchTerm = getSearchTerm(query);
if (searchTerm) {
@@ -482,6 +489,127 @@ const cancelTransaction = async (transactionId, actorId = null) => {
return getPaymentById(payment._id);
};
const createBulkClassPayments = async (body, actorId = null) => {
const { classId } = body;
if (!classId) {
throw new AppError('VALIDATION_FAILED', { classId: 'Class ID is required' }, 'شناسه کلاس الزامی است.');
}
const classDoc = await Class.findById(classId).populate('course', 'title price').lean();
if (!classDoc) {
throw new AppError('CLASS_NOT_FOUND', {}, 'کلاس مورد نظر یافت نشد.');
}
const allStudentIds = (classDoc.students || []).map((s) => String(s._id || s.id || s));
const targetStudentIds = (body.studentIds && Array.isArray(body.studentIds) && body.studentIds.length > 0)
? body.studentIds.map(String).filter((id) => allStudentIds.includes(id))
: allStudentIds;
if (!targetStudentIds.length) {
return {
message: 'هیچ دانشجویی در این کلاس ثبت‌نام نشده است.',
totalStudents: 0,
createdCount: 0,
skippedCount: 0,
payments: []
};
}
const skipExisting = body.skipExisting !== false;
let existingUserIds = new Set();
if (skipExisting) {
const existing = await Payment.find({
classes: classId,
user: { $in: targetStudentIds },
isDeleted: { $ne: true }
}).select('user').lean();
existingUserIds = new Set(existing.map((p) => String(p.user)));
}
const amount = body.amount !== undefined && body.amount !== null && body.amount !== ''
? Number(body.amount)
: (classDoc.tuitionFee || classDoc.course?.price || 0);
const discount = body.discount !== undefined && body.discount !== null && body.discount !== ''
? Number(body.discount)
: (classDoc.hasDiscount ? (classDoc.discount || 0) : 0);
const dueDate = parseDate(body.dueDate) || classDoc.startDate || new Date();
const notify = body.notify || {};
const notifySms = body.notifySms;
const notifyEmail = body.notifyEmail;
const notifyBot = body.notifyBot;
const createdPayments = [];
let skippedCount = 0;
for (const studentId of targetStudentIds) {
if (skipExisting && existingUserIds.has(studentId)) {
skippedCount++;
continue;
}
const payment = await createPayment({
user: studentId,
classes: [classId],
course: classDoc.course?._id || classDoc.course,
amount,
discount,
dueDate,
notes: body.notes,
notify,
notifySms,
notifyEmail,
notifyBot
}, actorId);
createdPayments.push(payment);
}
return {
totalStudents: targetStudentIds.length,
createdCount: createdPayments.length,
skippedCount,
payments: createdPayments
};
};
const checkDuplicatePayment = async (query = {}) => {
const { userId, classId, classes } = query;
if (!userId) {
return { hasDuplicate: false, count: 0, payments: [] };
}
const filter = {
user: userId,
isDeleted: { $ne: true }
};
const targetClasses = [];
if (classId) targetClasses.push(classId);
if (classes) {
if (Array.isArray(classes)) targetClasses.push(...classes);
else targetClasses.push(...String(classes).split(',').map((s) => s.trim()).filter(Boolean));
}
if (targetClasses.length) {
filter.classes = { $in: targetClasses };
}
const payments = await Payment.find(filter)
.populate({ path: 'classes', select: 'name' })
.populate({ path: 'course', select: 'title' })
.select('_id uniqueCode amount discount paidAmount status classes course createdAt')
.lean();
return {
hasDuplicate: payments.length > 0,
count: payments.length,
payments
};
};
const getMyPayments = async (userId, query = {}) => {
return getAllPayments({ ...query, userId });
};
@@ -490,6 +618,8 @@ module.exports = {
getAllPayments,
getPaymentById,
createPayment,
createBulkClassPayments,
checkDuplicatePayment,
updatePayment,
deletePayment,
searchPayments,