feat(payments): add bulk class payments creation and duplicate check
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user