feat: add soft delete for classes with cascade to sessions and linked records

This commit is contained in:
2026-08-21 13:19:50 +03:30
parent 5b56171328
commit e34fd25af7
15 changed files with 184 additions and 33 deletions
+40 -14
View File
@@ -4,6 +4,8 @@
const Class = require('./classModel');
const User = require('../users/userModel');
const Session = require('../sessions/sessionModel');
const PendingStudent = require('../pendingStudents/pendingStudentModel');
const Payment = require('../payments/paymentModel');
const AppError = require('../../utils/AppError');
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
@@ -19,7 +21,7 @@ const applyScheduleFields = (payload, body) => {
return payload;
};
const CLASS_LIST_FIELDS = 'name course professor students capacity tuitionFee hasDiscount discount freeSpots showOnFrontend startDate endDate days startTime endTime numberOfSessions payoutType payoutPercentage payoutHourlyRate extraExpensePerSession isActive adminNotes createdAt updatedAt';
const CLASS_LIST_FIELDS = 'name course professor students capacity tuitionFee hasDiscount discount freeSpots showOnFrontend startDate endDate days startTime endTime numberOfSessions payoutType payoutPercentage payoutHourlyRate extraExpensePerSession isActive isDeleted deletedAt adminNotes createdAt updatedAt';
const normalizePricingFields = (body = {}) => {
const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0);
@@ -56,12 +58,18 @@ const enrichClassForDisplay = (cls) => {
return { ...cls, finalTuitionFee, daysUntilStart };
};
const getAll = async (query) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200);
const getAll = async (query = {}) => {
const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit, 10) || 20, 200);
const skip = (page - 1) * limit;
const filter = {};
if (query.trash === 'true' || query.isDeleted === 'true') {
filter.isDeleted = true;
} else {
filter.isDeleted = { $ne: true };
}
if (query.courseId) filter.course = query.courseId;
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
@@ -130,8 +138,24 @@ const update = async (id, body) => {
};
const remove = async (id) => {
const cls = await Class.findByIdAndDelete(id);
const cls = await Class.findById(id);
if (!cls) throw new AppError('CLASS_NOT_FOUND');
const now = new Date();
await Class.findByIdAndUpdate(id, { $set: { isDeleted: true, deletedAt: now } });
await Session.updateMany({ class: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
await PendingStudent.updateMany({ class: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
await Payment.updateMany({ classes: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
return { success: true };
};
const restore = async (id) => {
const cls = await Class.findById(id);
if (!cls) throw new AppError('CLASS_NOT_FOUND');
await Class.findByIdAndUpdate(id, { $set: { isDeleted: false, deletedAt: null } });
await Session.updateMany({ class: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
await PendingStudent.updateMany({ class: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
await Payment.updateMany({ classes: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
return getOne(id);
};
const registerUsers = async (classId, userIds, notifyInput = {}) => {
@@ -151,7 +175,7 @@ const registerUsers = async (classId, userIds, notifyInput = {}) => {
if (notify.sms || notify.email || notify.bot) {
const classLabel = cls.name || cls.course?.title || 'کلاس';
const sessions = await Session.find({ class: classId })
const sessions = await Session.find({ class: classId, isDeleted: { $ne: true } })
.select('day startTime endTime')
.sort({ day: 1 })
.lean();
@@ -200,11 +224,11 @@ const removeUser = async (classId, userId) => {
};
const getMyClasses = async (userId, query = {}) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200);
const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit, 10) || 20, 200);
const skip = (page - 1) * limit;
const filter = { students: userId };
const filter = { students: userId, isDeleted: { $ne: true } };
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
const [items, total] = await Promise.all([
@@ -219,13 +243,14 @@ const getMyClasses = async (userId, query = {}) => {
};
const getPublicClasses = async (query = {}) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 50, 200);
const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit, 10) || 50, 200);
const skip = (page - 1) * limit;
const filter = {
isActive: { $ne: false },
showOnFrontend: { $ne: false }
showOnFrontend: { $ne: false },
isDeleted: { $ne: true }
};
if (query.courseId) filter.course = query.courseId;
@@ -245,7 +270,8 @@ const getPublicOne = async (id) => {
const cls = await Class.findOne({
_id: id,
isActive: { $ne: false },
showOnFrontend: { $ne: false }
showOnFrontend: { $ne: false },
isDeleted: { $ne: true }
})
.select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime isActive')
.populate({ path: 'course', select: 'title type description sectionCount hoursPerSection price' })
@@ -256,4 +282,4 @@ const getPublicOne = async (id) => {
return enrichClassForDisplay(cls);
};
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses, getPublicOne };
module.exports = { getAll, getOne, create, update, remove, restore, registerUsers, removeUser, getMyClasses, getPublicClasses, getPublicOne };