feat: add pending student registration flow and pricing APIs.
Support public class lookup, user provisioning, installment pricing, and admin approval before final enrollment.
This commit is contained in:
@@ -49,3 +49,8 @@ exports.getPublicClasses = catchAsync(async (req, res) => {
|
||||
const { data, meta } = await classService.getPublicClasses(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.getPublicOne = catchAsync(async (req, res) => {
|
||||
const cls = await classService.getPublicOne(req.params.id);
|
||||
return successResponse(res, 200, 'Class retrieved successfully', cls);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const router = express.Router();
|
||||
|
||||
// Public Scope
|
||||
router.get('/user/get-all', classController.getPublicClasses);
|
||||
router.get('/user/get-one/:id', classController.getPublicOne);
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
||||
@@ -202,4 +202,19 @@ const getPublicClasses = async (query = {}) => {
|
||||
return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) };
|
||||
};
|
||||
|
||||
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses };
|
||||
const getPublicOne = async (id) => {
|
||||
const cls = await Class.findOne({
|
||||
_id: id,
|
||||
isActive: { $ne: false },
|
||||
showOnFrontend: { $ne: false }
|
||||
})
|
||||
.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' })
|
||||
.populate({ path: 'professor', select: 'name surname' })
|
||||
.lean();
|
||||
|
||||
if (!cls) throw new AppError('CLASS_NOT_FOUND', null, 'کلاس یافت نشد یا برای ثبتنام در دسترس نیست.');
|
||||
return enrichClassForDisplay(cls);
|
||||
};
|
||||
|
||||
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses, getPublicOne };
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const pendingStudentService = require('./pendingStudentService');
|
||||
const { successResponse, listResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.getPricing = catchAsync(async (req, res) => {
|
||||
const preview = await pendingStudentService.getPricingPreview({
|
||||
classId: req.query.classId,
|
||||
type: req.query.type || 'enrollment',
|
||||
paymentPlan: req.query.paymentPlan || 'full'
|
||||
});
|
||||
return successResponse(res, 200, 'Pricing preview retrieved successfully', preview);
|
||||
});
|
||||
|
||||
exports.ensureUser = catchAsync(async (req, res) => {
|
||||
const result = await pendingStudentService.findOrCreateUser(req.body);
|
||||
return successResponse(res, 200, 'User ensured successfully', result);
|
||||
});
|
||||
|
||||
exports.completeAfterPayment = catchAsync(async (req, res) => {
|
||||
const pending = await pendingStudentService.createPendingStudentAfterPayment(req.body);
|
||||
return successResponse(res, 201, 'Registration request submitted successfully', pending);
|
||||
});
|
||||
|
||||
exports.getAll = catchAsync(async (req, res) => {
|
||||
const { data, meta } = await pendingStudentService.getAllPendingStudents(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.getOne = catchAsync(async (req, res) => {
|
||||
const pending = await pendingStudentService.getPendingStudentById(req.params.id);
|
||||
return successResponse(res, 200, 'Pending student retrieved successfully', pending);
|
||||
});
|
||||
|
||||
exports.approve = catchAsync(async (req, res) => {
|
||||
const result = await pendingStudentService.approvePendingStudent(req.params.id, req.user._id, req.body);
|
||||
return successResponse(res, 200, 'Pending student approved successfully', result);
|
||||
});
|
||||
|
||||
exports.reject = catchAsync(async (req, res) => {
|
||||
const result = await pendingStudentService.rejectPendingStudent(req.params.id, req.user._id, req.body);
|
||||
return successResponse(res, 200, 'Pending student rejected successfully', result);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const pendingStudentSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
class: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Class',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
course: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Course',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['enrollment', 'class_request'],
|
||||
required: true
|
||||
},
|
||||
paymentPlan: {
|
||||
type: String,
|
||||
enum: ['full', 'installments', 'deposit'],
|
||||
required: true
|
||||
},
|
||||
tuitionFee: { type: Number, required: true, min: 0 },
|
||||
classDiscount: { type: Number, default: 0, min: 0 },
|
||||
paymentDiscount: { type: Number, default: 0, min: 0 },
|
||||
totalAmount: { type: Number, required: true, min: 0 },
|
||||
amountDueNow: { type: Number, required: true, min: 0 },
|
||||
secondInstallmentAmount: { type: Number, default: 0, min: 0 },
|
||||
secondInstallmentDueDate: { type: Date },
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending_payment', 'pending_review', 'approved', 'rejected', 'cancelled'],
|
||||
default: 'pending_review',
|
||||
index: true
|
||||
},
|
||||
payment: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Payment'
|
||||
},
|
||||
paymentReference: { type: String, trim: true },
|
||||
adminNotes: { type: String, trim: true, maxlength: 2000 },
|
||||
reviewedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
||||
reviewedAt: { type: Date }
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
pendingStudentSchema.index({ user: 1, class: 1, type: 1, status: 1 });
|
||||
|
||||
module.exports = mongoose.model('PendingStudent', pendingStudentSchema);
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const pendingStudentController = require('./pendingStudentController');
|
||||
const authMiddleware = require('../../middlewares/authMiddleware');
|
||||
const perm = require('../../middlewares/permissionMiddleware');
|
||||
const { PERMISSIONS } = require('../../constants/permissions');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/public/pricing', pendingStudentController.getPricing);
|
||||
router.post('/public/ensure-user', pendingStudentController.ensureUser);
|
||||
router.post('/public/complete', pendingStudentController.completeAfterPayment);
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get(
|
||||
'/admin/get-all',
|
||||
perm.requires(PERMISSIONS.PENDING_STUDENTS_READ),
|
||||
pendingStudentController.getAll
|
||||
);
|
||||
router.get(
|
||||
'/admin/get-one/:id',
|
||||
perm.requires(PERMISSIONS.PENDING_STUDENTS_READ),
|
||||
pendingStudentController.getOne
|
||||
);
|
||||
router.post(
|
||||
'/admin/:id/approve',
|
||||
perm.requires(PERMISSIONS.PENDING_STUDENTS_UPDATE),
|
||||
pendingStudentController.approve
|
||||
);
|
||||
router.post(
|
||||
'/admin/:id/reject',
|
||||
perm.requires(PERMISSIONS.PENDING_STUDENTS_UPDATE),
|
||||
pendingStudentController.reject
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,355 @@
|
||||
'use strict';
|
||||
|
||||
const PendingStudent = require('./pendingStudentModel');
|
||||
const Class = require('../classes/classModel');
|
||||
const User = require('../users/userModel');
|
||||
const Role = require('../roles/roleModel');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||
const { calculateRegistrationPricing } = require('../../utils/registrationPricing');
|
||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
|
||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||
const classService = require('../classes/classService');
|
||||
const paymentService = require('../payments/paymentService');
|
||||
const logger = require('../../utils/logger');
|
||||
const { recordAndSend } = require('../../utils/senders/notificationRecorder');
|
||||
|
||||
const POPULATE_LIST = [
|
||||
{ path: 'user', select: 'name phoneNumber nationalIdCode email username' },
|
||||
{ path: 'class', select: 'name tuitionFee startDate days startTime endTime', populate: { path: 'course', select: 'title sectionCount' } },
|
||||
{ path: 'course', select: 'title sectionCount' },
|
||||
{ path: 'reviewedBy', select: 'name username' }
|
||||
];
|
||||
|
||||
const normalizePhone = (value) => String(value || '').replace(/[\s\-()]/g, '').trim();
|
||||
|
||||
const isValidNationalId = (code) => {
|
||||
if (!/^\d{10}$/.test(code)) return false;
|
||||
if (/^(\d)\1{9}$/.test(code)) return false;
|
||||
const check = Number(code[9]);
|
||||
const sum = code
|
||||
.split('')
|
||||
.slice(0, 9)
|
||||
.reduce((acc, digit, index) => acc + Number(digit) * (10 - index), 0);
|
||||
const remainder = sum % 11;
|
||||
return (remainder < 2 && check === remainder) || (remainder >= 2 && check === 11 - remainder);
|
||||
};
|
||||
|
||||
const getPublicClassForRegistration = async (classId) => {
|
||||
const cls = await Class.findOne({
|
||||
_id: classId,
|
||||
isActive: { $ne: false },
|
||||
showOnFrontend: { $ne: false }
|
||||
})
|
||||
.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' })
|
||||
.populate({ path: 'professor', select: 'name surname' })
|
||||
.lean();
|
||||
|
||||
if (!cls) throw new AppError('CLASS_NOT_FOUND', null, 'کلاس یافت نشد یا برای ثبتنام در دسترس نیست.');
|
||||
return cls;
|
||||
};
|
||||
|
||||
const getPricingPreview = async ({ classId, type = 'enrollment', paymentPlan = 'full' }) => {
|
||||
const cls = await getPublicClassForRegistration(classId);
|
||||
const pricing = calculateRegistrationPricing(cls, cls.course || {}, { type, paymentPlan });
|
||||
return {
|
||||
class: {
|
||||
_id: cls._id,
|
||||
name: cls.name,
|
||||
course: cls.course,
|
||||
professor: cls.professor,
|
||||
startDate: cls.startDate,
|
||||
days: cls.days,
|
||||
startTime: cls.startTime,
|
||||
endTime: cls.endTime,
|
||||
finalTuitionFee: pricing.tuitionFee
|
||||
},
|
||||
pricing
|
||||
};
|
||||
};
|
||||
|
||||
const allocateUniqueUsername = async () => {
|
||||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||||
const username = generateUsername();
|
||||
const exists = await User.exists({ username });
|
||||
if (!exists) return username;
|
||||
}
|
||||
throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username');
|
||||
};
|
||||
|
||||
const findOrCreateUser = async (body) => {
|
||||
const name = mergeFullName(body.name, body.surname);
|
||||
const nationalIdCode = String(body.nationalIdCode || body.nationalId || '').trim();
|
||||
const phoneNumber = normalizePhone(body.phoneNumber || body.phone);
|
||||
const email = body.email ? String(body.email).trim().toLowerCase() : undefined;
|
||||
const gender = normalizeGender(body.gender);
|
||||
|
||||
if (!name) throw new AppError('VALIDATION_FAILED', { name: 'نام و نام خانوادگی الزامی است' });
|
||||
if (!nationalIdCode) throw new AppError('VALIDATION_FAILED', { nationalIdCode: 'کد ملی الزامی است' });
|
||||
if (!isValidNationalId(nationalIdCode)) {
|
||||
throw new AppError('VALIDATION_FAILED', { nationalIdCode: 'کد ملی معتبر نیست' });
|
||||
}
|
||||
if (!phoneNumber) throw new AppError('VALIDATION_FAILED', { phoneNumber: 'شماره موبایل الزامی است' });
|
||||
if (!/^(0?9\d{9}|\+989\d{9})$/.test(phoneNumber)) {
|
||||
throw new AppError('VALIDATION_FAILED', { phoneNumber: 'شماره موبایل معتبر نیست' });
|
||||
}
|
||||
|
||||
const normalizedPhone = phoneNumber.startsWith('+98')
|
||||
? `0${phoneNumber.slice(3)}`
|
||||
: phoneNumber.startsWith('9') && phoneNumber.length === 10
|
||||
? `0${phoneNumber}`
|
||||
: phoneNumber;
|
||||
|
||||
let user = await User.findOne({
|
||||
$or: [{ phoneNumber: normalizedPhone }, { nationalIdCode }]
|
||||
});
|
||||
|
||||
if (user) {
|
||||
const updates = {};
|
||||
if (user.name !== name) updates.name = name;
|
||||
if (email && user.email !== email) updates.email = email;
|
||||
if (gender && user.gender !== gender) updates.gender = gender;
|
||||
if (user.phoneNumber !== normalizedPhone) updates.phoneNumber = normalizedPhone;
|
||||
if (Object.keys(updates).length) {
|
||||
user = await User.findByIdAndUpdate(user._id, updates, { new: true, runValidators: true });
|
||||
}
|
||||
return {
|
||||
user: {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
phoneNumber: user.phoneNumber,
|
||||
nationalIdCode: user.nationalIdCode,
|
||||
email: user.email
|
||||
},
|
||||
isNewUser: false
|
||||
};
|
||||
}
|
||||
|
||||
const userRole = await Role.findOne({ name: 'User' });
|
||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||
|
||||
const username = await allocateUniqueUsername();
|
||||
const plainPassword = generateSimplePassword();
|
||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
user = await User.create({
|
||||
name,
|
||||
nationalIdCode,
|
||||
phoneNumber: normalizedPhone,
|
||||
email,
|
||||
gender,
|
||||
username,
|
||||
passwordHash,
|
||||
role: userRole._id,
|
||||
preferredMessenger: ['SMS']
|
||||
});
|
||||
|
||||
try {
|
||||
await sendAccountCreatedSms(normalizedPhone, username, plainPassword, user._id);
|
||||
} catch (err) {
|
||||
logger.error(`[findOrCreateUser] Account SMS failed for ${normalizedPhone}: ${err.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
phoneNumber: user.phoneNumber,
|
||||
nationalIdCode: user.nationalIdCode,
|
||||
email: user.email
|
||||
},
|
||||
isNewUser: true
|
||||
};
|
||||
};
|
||||
|
||||
const createPendingStudentAfterPayment = async (body) => {
|
||||
const {
|
||||
userId,
|
||||
classId,
|
||||
type = 'enrollment',
|
||||
paymentPlan = 'full',
|
||||
paymentReference
|
||||
} = body;
|
||||
|
||||
if (!userId || !classId) {
|
||||
throw new AppError('VALIDATION_FAILED', { form: 'اطلاعات ثبتنام ناقص است' });
|
||||
}
|
||||
|
||||
const [user, cls] = await Promise.all([
|
||||
User.findById(userId).select('_id name phoneNumber').lean(),
|
||||
getPublicClassForRegistration(classId)
|
||||
]);
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
|
||||
const resolvedPlan = type === 'class_request' ? 'deposit' : paymentPlan;
|
||||
const pricing = calculateRegistrationPricing(cls, cls.course || {}, { type, paymentPlan: resolvedPlan });
|
||||
|
||||
const existing = await PendingStudent.findOne({
|
||||
user: userId,
|
||||
class: classId,
|
||||
type,
|
||||
status: { $in: ['pending_payment', 'pending_review'] }
|
||||
});
|
||||
if (existing) {
|
||||
throw new AppError('DUPLICATE_KEY', null, 'درخواست فعال برای این کلاس از قبل ثبت شده است.');
|
||||
}
|
||||
|
||||
const pending = await PendingStudent.create({
|
||||
user: userId,
|
||||
class: classId,
|
||||
course: cls.course._id,
|
||||
type,
|
||||
paymentPlan: pricing.paymentPlan,
|
||||
tuitionFee: pricing.tuitionFee,
|
||||
classDiscount: pricing.classDiscount,
|
||||
paymentDiscount: pricing.paymentDiscount,
|
||||
totalAmount: pricing.totalAmount,
|
||||
amountDueNow: pricing.amountDueNow,
|
||||
secondInstallmentAmount: pricing.secondInstallmentAmount,
|
||||
secondInstallmentDueDate: pricing.secondInstallmentDueDate,
|
||||
status: 'pending_review',
|
||||
paymentReference: paymentReference || undefined
|
||||
});
|
||||
|
||||
return PendingStudent.findById(pending._id).populate(POPULATE_LIST).lean();
|
||||
};
|
||||
|
||||
const getAllPendingStudents = 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.status) filter.status = query.status;
|
||||
if (query.type) filter.type = query.type;
|
||||
if (query.classId) filter.class = query.classId;
|
||||
|
||||
const searchTerm = getSearchTerm(query);
|
||||
if (searchTerm) {
|
||||
const searchRegex = new RegExp(escapeRegex(searchTerm), 'i');
|
||||
const matchedUsers = await User.find({
|
||||
$or: [
|
||||
{ name: searchRegex },
|
||||
{ phoneNumber: searchRegex },
|
||||
{ nationalIdCode: searchRegex }
|
||||
]
|
||||
}).select('_id').lean();
|
||||
filter.$or = [
|
||||
{ adminNotes: searchRegex },
|
||||
{ user: { $in: matchedUsers.map((u) => u._id) } }
|
||||
];
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
PendingStudent.find(filter)
|
||||
.populate(POPULATE_LIST)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.sort({ createdAt: -1 })
|
||||
.lean(),
|
||||
PendingStudent.countDocuments(filter)
|
||||
]);
|
||||
|
||||
return { data: items, meta: calculateMeta(total, page, limit) };
|
||||
};
|
||||
|
||||
const getPendingStudentById = async (id) => {
|
||||
const pending = await PendingStudent.findById(id).populate(POPULATE_LIST).lean();
|
||||
if (!pending) throw new AppError('NOT_FOUND', null, 'درخواست در انتظار یافت نشد.');
|
||||
return pending;
|
||||
};
|
||||
|
||||
const sendClassRequestApprovedSms = async (user, courseTitle) => {
|
||||
if (!user?.phoneNumber) return;
|
||||
const body = `درخواست تشکیل کلاس جدید برای «${courseTitle}» تأیید شد. بهزودی با شما هماهنگ میکنیم.`;
|
||||
await recordAndSend({
|
||||
userId: user._id,
|
||||
channel: 'sms',
|
||||
subject: 'تأیید درخواست کلاس',
|
||||
body,
|
||||
relatedEvent: 'class_request.approved',
|
||||
sendFn: async () => {
|
||||
logger.info(`[class_request.approved] SMS queued for ${user.phoneNumber}: ${body}`);
|
||||
return { skipped: true, reason: 'template_pending' };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const approvePendingStudent = async (id, actorId, body = {}) => {
|
||||
const pending = await PendingStudent.findById(id)
|
||||
.populate({ path: 'user', select: 'name phoneNumber' })
|
||||
.populate({ path: 'class', select: 'name course', populate: { path: 'course', select: 'title' } })
|
||||
.populate({ path: 'course', select: 'title' });
|
||||
|
||||
if (!pending) throw new AppError('NOT_FOUND', null, 'درخواست در انتظار یافت نشد.');
|
||||
if (!['pending_review', 'pending_payment'].includes(pending.status)) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'این درخواست قابل تأیید نیست.');
|
||||
}
|
||||
|
||||
if (pending.type === 'enrollment') {
|
||||
await classService.registerUsers(pending.class._id, [pending.user._id], { sendSms: true });
|
||||
|
||||
const transactions = [{
|
||||
amount: pending.amountDueNow,
|
||||
status: 'paid',
|
||||
method: 'online',
|
||||
date: new Date(),
|
||||
notes: 'پرداخت اولیه — تأیید توسط آموزشگاه'
|
||||
}];
|
||||
|
||||
if (pending.paymentPlan === 'installments' && pending.secondInstallmentAmount > 0) {
|
||||
transactions.push({
|
||||
amount: pending.secondInstallmentAmount,
|
||||
status: 'pending',
|
||||
dueDate: pending.secondInstallmentDueDate || new Date(),
|
||||
notes: 'قسط دوم'
|
||||
});
|
||||
}
|
||||
|
||||
await paymentService.createPayment({
|
||||
user: pending.user._id,
|
||||
classes: [pending.class._id],
|
||||
course: pending.course,
|
||||
amount: pending.paymentPlan === 'installments' ? pending.totalAmount : pending.tuitionFee,
|
||||
discount: pending.paymentDiscount,
|
||||
notes: body.adminNotes || pending.adminNotes || '',
|
||||
transactions,
|
||||
sendSms: false
|
||||
}, actorId);
|
||||
} else {
|
||||
const courseTitle = pending.course?.title || pending.class?.course?.title || pending.class?.name || 'دوره';
|
||||
await sendClassRequestApprovedSms(pending.user, courseTitle);
|
||||
}
|
||||
|
||||
await PendingStudent.findByIdAndDelete(id);
|
||||
|
||||
return { approved: true, type: pending.type };
|
||||
};
|
||||
|
||||
const rejectPendingStudent = async (id, actorId, body = {}) => {
|
||||
const pending = await PendingStudent.findById(id);
|
||||
if (!pending) throw new AppError('NOT_FOUND', null, 'درخواست در انتظار یافت نشد.');
|
||||
if (!['pending_review', 'pending_payment'].includes(pending.status)) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'این درخواست قابل رد کردن نیست.');
|
||||
}
|
||||
|
||||
if (body.adminNotes != null) {
|
||||
logger.info(`[rejectPendingStudent] ${id} by ${actorId}: ${String(body.adminNotes).trim()}`);
|
||||
}
|
||||
|
||||
await PendingStudent.findByIdAndDelete(id);
|
||||
return { rejected: true };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPricingPreview,
|
||||
findOrCreateUser,
|
||||
createPendingStudentAfterPayment,
|
||||
getAllPendingStudents,
|
||||
getPendingStudentById,
|
||||
approvePendingStudent,
|
||||
rejectPendingStudent
|
||||
};
|
||||
Reference in New Issue
Block a user