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:
2026-08-16 08:42:25 +03:30
parent fafc362689
commit cd98adff84
13 changed files with 678 additions and 3 deletions
+94
View File
@@ -0,0 +1,94 @@
'use strict';
const { FULL_PAYMENT_DISCOUNT, CLASS_REQUEST_DEPOSIT_RATE } = require('../constants/registration');
const getFinalTuition = (cls = {}) => {
const tuitionFee = Math.max(0, Number(cls.tuitionFee) || 0);
const classDiscount = cls.hasDiscount ? Math.max(0, Number(cls.discount) || 0) : 0;
return Math.max(0, tuitionFee - Math.min(classDiscount, tuitionFee));
};
const calculateMidpointDueDate = (cls = {}, course = {}) => {
const startDate = cls.startDate ? new Date(cls.startDate) : null;
if (!startDate || Number.isNaN(startDate.getTime())) return null;
const sectionCount = Math.max(1, Number(course.sectionCount) || 1);
const sessionsPerWeek = Math.max(1, (cls.days || []).length || 1);
const totalWeeks = sectionCount / sessionsPerWeek;
const weeksUntilDue = totalWeeks / 2;
const due = new Date(startDate);
due.setDate(due.getDate() + Math.round(weeksUntilDue * 7));
return due;
};
const calculateEnrollmentPricing = (cls = {}, course = {}, paymentPlan = 'full') => {
const tuitionFee = getFinalTuition(cls);
if (paymentPlan === 'installments') {
const totalAmount = tuitionFee;
const amountDueNow = Math.ceil(totalAmount / 2);
const secondInstallmentAmount = Math.max(0, totalAmount - amountDueNow);
return {
type: 'enrollment',
paymentPlan: 'installments',
tuitionFee,
classDiscount: cls.hasDiscount ? Math.max(0, Number(cls.discount) || 0) : 0,
paymentDiscount: 0,
totalAmount,
amountDueNow,
secondInstallmentAmount,
secondInstallmentDueDate: calculateMidpointDueDate(cls, course)
};
}
const paymentDiscount = Math.min(FULL_PAYMENT_DISCOUNT, tuitionFee);
const totalAmount = Math.max(0, tuitionFee - paymentDiscount);
return {
type: 'enrollment',
paymentPlan: 'full',
tuitionFee,
classDiscount: cls.hasDiscount ? Math.max(0, Number(cls.discount) || 0) : 0,
paymentDiscount,
totalAmount,
amountDueNow: totalAmount,
secondInstallmentAmount: 0,
secondInstallmentDueDate: null
};
};
const calculateClassRequestPricing = (cls = {}) => {
const tuitionFee = getFinalTuition(cls);
const amountDueNow = Math.ceil(tuitionFee * CLASS_REQUEST_DEPOSIT_RATE);
return {
type: 'class_request',
paymentPlan: 'deposit',
tuitionFee,
classDiscount: cls.hasDiscount ? Math.max(0, Number(cls.discount) || 0) : 0,
paymentDiscount: 0,
totalAmount: amountDueNow,
amountDueNow,
secondInstallmentAmount: 0,
secondInstallmentDueDate: null
};
};
const calculateRegistrationPricing = (cls = {}, course = {}, { type = 'enrollment', paymentPlan = 'full' } = {}) => {
if (type === 'class_request') {
return calculateClassRequestPricing(cls);
}
return calculateEnrollmentPricing(cls, course, paymentPlan);
};
module.exports = {
FULL_PAYMENT_DISCOUNT,
CLASS_REQUEST_DEPOSIT_RATE,
getFinalTuition,
calculateMidpointDueDate,
calculateEnrollmentPricing,
calculateClassRequestPricing,
calculateRegistrationPricing
};
+46
View File
@@ -0,0 +1,46 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const {
calculateEnrollmentPricing,
calculateClassRequestPricing,
calculateMidpointDueDate
} = require('./registrationPricing');
test('full payment applies 500k discount', () => {
const result = calculateEnrollmentPricing(
{ tuitionFee: 5_000_000, hasDiscount: false },
{ sectionCount: 16 },
'full'
);
assert.equal(result.totalAmount, 4_500_000);
assert.equal(result.paymentDiscount, 500_000);
assert.equal(result.amountDueNow, 4_500_000);
});
test('installments charge full tuition with half due now', () => {
const result = calculateEnrollmentPricing(
{ tuitionFee: 4_000_000, hasDiscount: false, startDate: '2026-09-01', days: [2, 4] },
{ sectionCount: 16 },
'installments'
);
assert.equal(result.totalAmount, 4_000_000);
assert.equal(result.amountDueNow, 2_000_000);
assert.equal(result.secondInstallmentAmount, 2_000_000);
assert.ok(result.secondInstallmentDueDate);
});
test('class request deposit is 10 percent', () => {
const result = calculateClassRequestPricing({ tuitionFee: 3_000_000, hasDiscount: false });
assert.equal(result.amountDueNow, 300_000);
assert.equal(result.paymentPlan, 'deposit');
});
test('midpoint due date is halfway through course duration', () => {
const due = calculateMidpointDueDate(
{ startDate: '2026-01-01', days: [2, 4] },
{ sectionCount: 16 }
);
assert.equal(due.toISOString().slice(0, 10), '2026-01-29');
});