feat: add class discounts, frontend visibility, and public listing

Add hasDiscount, freeSpots, and showOnFrontend on classes with a public get-all endpoint and richer notification user populate.
This commit is contained in:
2026-08-16 08:11:42 +03:30
parent 21bff0c4ba
commit f57d8bbdfa
5 changed files with 100 additions and 10 deletions
+5
View File
@@ -44,3 +44,8 @@ exports.getMyClasses = catchAsync(async (req, res) => {
const { data, meta } = await classService.getMyClasses(req.user._id, req.query);
return listResponse(res, 200, data, meta);
});
exports.getPublicClasses = catchAsync(async (req, res) => {
const { data, meta } = await classService.getPublicClasses(req.query);
return listResponse(res, 200, data, meta);
});
+28 -1
View File
@@ -30,6 +30,25 @@ const classSchema = new mongoose.Schema({
type: Number,
default: 0
},
hasDiscount: {
type: Boolean,
default: false
},
discount: {
type: Number,
default: 0,
min: 0
},
freeSpots: {
type: Number,
min: 0,
default: null
},
showOnFrontend: {
type: Boolean,
default: true,
index: true
},
startDate: {
type: Date
},
@@ -63,7 +82,15 @@ const classSchema = new mongoose.Schema({
default: []
}
}, {
timestamps: true
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
classSchema.virtual('finalTuitionFee').get(function () {
const tuition = this.tuitionFee || 0;
if (!this.hasDiscount) return tuition;
return Math.max(0, tuition - (this.discount || 0));
});
module.exports = mongoose.model('Class', classSchema);
+3
View File
@@ -9,6 +9,9 @@ const { PERMISSIONS } = require('../../constants/permissions');
const router = express.Router();
// Public Scope
router.get('/user/get-all', classController.getPublicClasses);
router.use(authMiddleware);
// User Scope
+63 -8
View File
@@ -18,6 +18,28 @@ 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 isActive adminNotes createdAt updatedAt';
const normalizePricingFields = (body = {}) => {
const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0);
const hasDiscount = Boolean(body.hasDiscount);
const discount = hasDiscount
? Math.min(Math.max(0, Number(body.discount) || 0), tuitionFee)
: 0;
return { tuitionFee, hasDiscount, discount };
};
const enrichClassForDisplay = (cls) => {
const tuitionFee = cls.tuitionFee || 0;
const discount = cls.hasDiscount ? (cls.discount || 0) : 0;
const finalTuitionFee = Math.max(0, tuitionFee - discount);
let daysUntilStart = null;
if (cls.startDate) {
daysUntilStart = Math.ceil((new Date(cls.startDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
}
return { ...cls, finalTuitionFee, daysUntilStart };
};
const getAll = async (query) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200);
@@ -34,14 +56,14 @@ const getAll = async (query) => {
const [items, total] = await Promise.all([
Class.find(filter)
.select('name course professor students capacity tuitionFee startDate endDate days startTime endTime isActive adminNotes createdAt updatedAt')
.select(CLASS_LIST_FIELDS)
.populate({ path: 'course', select: 'title type price' })
.populate({ path: 'professor', select: 'name surname' })
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
return { data: items, meta: calculateMeta(total, page, limit) };
return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) };
};
const getOne = async (id) => {
@@ -51,22 +73,32 @@ const getOne = async (id) => {
.populate({ path: 'students', select: 'name phoneNumber gender' })
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND');
return cls;
return enrichClassForDisplay(cls);
};
const create = async (body) => {
const payload = applyScheduleFields({ ...body }, body);
const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body);
if (body.freeSpots === '' || body.freeSpots === null || body.freeSpots === undefined) {
payload.freeSpots = null;
} else {
payload.freeSpots = Math.max(0, Number(body.freeSpots) || 0);
}
const cls = await Class.create(payload);
return getOne(cls._id);
};
const update = async (id, body) => {
const payload = applyScheduleFields({ ...body }, body);
const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body);
if (body.freeSpots === '' || body.freeSpots === null) {
payload.freeSpots = null;
} else if (body.freeSpots !== undefined) {
payload.freeSpots = Math.max(0, Number(body.freeSpots) || 0);
}
const cls = await Class.findByIdAndUpdate(id, payload, { new: true, runValidators: true })
.populate({ path: 'course', select: 'title' })
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND');
return cls;
return enrichClassForDisplay(cls);
};
const remove = async (id) => {
@@ -144,7 +176,30 @@ const getMyClasses = async (userId, query = {}) => {
Class.countDocuments(filter)
]);
return { data: items, meta: calculateMeta(total, page, limit) };
return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) };
};
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses };
const getPublicClasses = async (query = {}) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 50, 200);
const skip = (page - 1) * limit;
const filter = {
isActive: { $ne: false },
showOnFrontend: { $ne: false }
};
if (query.courseId) filter.course = query.courseId;
const [items, total] = await Promise.all([
Class.find(filter)
.select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime')
.populate({ path: 'course', select: 'title type description' })
.populate({ path: 'professor', select: 'name surname' })
.skip(skip).limit(limit).sort({ startDate: 1, createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) };
};
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses };
@@ -49,7 +49,7 @@ const getAllNotifications = async (queryParams) => {
const filter = buildFilterQuery(queryParams, ['subject', 'body']);
const [notifications, totalCount] = await Promise.all([
Notification.find(filter).populate('user', 'name username').sort(sort).skip(skip).limit(limit),
Notification.find(filter).populate('user', 'name username phoneNumber').sort(sort).skip(skip).limit(limit),
Notification.countDocuments(filter)
]);