feat: add soft delete for classes with cascade to sessions and linked records
This commit is contained in:
@@ -30,6 +30,11 @@ exports.delete = catchAsync(async (req, res) => {
|
||||
return successResponse(res, 200, 'Class deleted successfully');
|
||||
});
|
||||
|
||||
exports.restore = catchAsync(async (req, res) => {
|
||||
const cls = await classService.restore(req.params.id);
|
||||
return successResponse(res, 200, 'Class restored successfully', cls);
|
||||
});
|
||||
|
||||
exports.registerUsers = catchAsync(async (req, res) => {
|
||||
const cls = await classService.registerUsers(req.params.id, req.body.userIds || [], req.body);
|
||||
return successResponse(res, 200, 'Users registered in class successfully', cls);
|
||||
|
||||
@@ -108,6 +108,15 @@ const classSchema = new mongoose.Schema({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
index: true
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
adminNotes: {
|
||||
type: [String],
|
||||
default: []
|
||||
|
||||
@@ -24,6 +24,7 @@ router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.CLASSES_READ), classC
|
||||
router.post('/admin/create', perm.requires(PERMISSIONS.CLASSES_CREATE), classController.create);
|
||||
router.put('/admin/update/:id', perm.requires(PERMISSIONS.CLASSES_UPDATE), classController.update);
|
||||
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.delete);
|
||||
router.post('/admin/restore/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.restore);
|
||||
router.post('/admin/:id/register-users', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.registerUsers);
|
||||
router.delete('/admin/:id/students/:userId', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.removeUser);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const Class = require('./classModel');
|
||||
const Session = require('../sessions/sessionModel');
|
||||
const PendingStudent = require('../pendingStudents/pendingStudentModel');
|
||||
const Payment = require('../payments/paymentModel');
|
||||
|
||||
describe('Soft delete schema configuration', () => {
|
||||
it('has isDeleted and deletedAt in Class schema', () => {
|
||||
const isDeletedPath = Class.schema.path('isDeleted');
|
||||
const deletedAtPath = Class.schema.path('deletedAt');
|
||||
|
||||
assert.ok(isDeletedPath, 'Class schema should have isDeleted path');
|
||||
assert.equal(isDeletedPath.instance, 'Boolean');
|
||||
assert.equal(isDeletedPath.defaultValue, false);
|
||||
|
||||
assert.ok(deletedAtPath, 'Class schema should have deletedAt path');
|
||||
assert.equal(deletedAtPath.instance, 'Date');
|
||||
assert.equal(deletedAtPath.defaultValue, null);
|
||||
});
|
||||
|
||||
it('has isDeleted and deletedAt in Session schema', () => {
|
||||
const isDeletedPath = Session.schema.path('isDeleted');
|
||||
const deletedAtPath = Session.schema.path('deletedAt');
|
||||
|
||||
assert.ok(isDeletedPath, 'Session schema should have isDeleted path');
|
||||
assert.equal(isDeletedPath.instance, 'Boolean');
|
||||
assert.equal(isDeletedPath.defaultValue, false);
|
||||
|
||||
assert.ok(deletedAtPath, 'Session schema should have deletedAt path');
|
||||
assert.equal(deletedAtPath.instance, 'Date');
|
||||
assert.equal(deletedAtPath.defaultValue, null);
|
||||
});
|
||||
|
||||
it('has isDeleted and deletedAt in PendingStudent schema', () => {
|
||||
const isDeletedPath = PendingStudent.schema.path('isDeleted');
|
||||
const deletedAtPath = PendingStudent.schema.path('deletedAt');
|
||||
|
||||
assert.ok(isDeletedPath, 'PendingStudent schema should have isDeleted path');
|
||||
assert.equal(isDeletedPath.instance, 'Boolean');
|
||||
assert.equal(isDeletedPath.defaultValue, false);
|
||||
|
||||
assert.ok(deletedAtPath, 'PendingStudent schema should have deletedAt path');
|
||||
assert.equal(deletedAtPath.instance, 'Date');
|
||||
assert.equal(deletedAtPath.defaultValue, null);
|
||||
});
|
||||
|
||||
it('has isDeleted and deletedAt in Payment schema', () => {
|
||||
const isDeletedPath = Payment.schema.path('isDeleted');
|
||||
const deletedAtPath = Payment.schema.path('deletedAt');
|
||||
|
||||
assert.ok(isDeletedPath, 'Payment schema should have isDeleted path');
|
||||
assert.equal(isDeletedPath.instance, 'Boolean');
|
||||
assert.equal(isDeletedPath.defaultValue, false);
|
||||
|
||||
assert.ok(deletedAtPath, 'Payment schema should have deletedAt path');
|
||||
assert.equal(deletedAtPath.instance, 'Date');
|
||||
assert.equal(deletedAtPath.defaultValue, null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user