Add professor share calculation and financial reporting feature
Introduces per-class payout configuration (percentage or hourly rate, plus an optional per-session expense allowance), pure calculation utilities for professor payout/net profit/receivables, and a new financial-reports API surface with per-class, per-session, and date-range/monthly reports. Also adds full CRUD for institutional expenses used in the range report, and wires up the new permissions and roles. Fixes a pre-existing ReferenceError (missing parseImportDate import) in paymentService that blocked creating payments with dated transactions.
This commit is contained in:
@@ -19,7 +19,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 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 adminNotes createdAt updatedAt';
|
||||
|
||||
const normalizePricingFields = (body = {}) => {
|
||||
const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0);
|
||||
@@ -30,6 +30,14 @@ const normalizePricingFields = (body = {}) => {
|
||||
return { tuitionFee, hasDiscount, discount };
|
||||
};
|
||||
|
||||
const normalizePayoutFields = (body = {}) => {
|
||||
const payoutType = body.payoutType === 'hourly' ? 'hourly' : 'percentage';
|
||||
const payoutPercentage = Math.min(100, Math.max(0, Number(body.payoutPercentage) || 0));
|
||||
const payoutHourlyRate = Math.max(0, Number(body.payoutHourlyRate) || 0);
|
||||
const extraExpensePerSession = Math.max(0, Number(body.extraExpensePerSession) || 0);
|
||||
return { payoutType, payoutPercentage, payoutHourlyRate, extraExpensePerSession };
|
||||
};
|
||||
|
||||
const normalizeNumberOfSessions = (value) => {
|
||||
if (value === '' || value === null || value === undefined) return null;
|
||||
const parsed = Number(value);
|
||||
@@ -65,7 +73,7 @@ const getAll = async (query) => {
|
||||
const [items, total] = await Promise.all([
|
||||
Class.find(filter)
|
||||
.select(CLASS_LIST_FIELDS)
|
||||
.populate({ path: 'course', select: 'title type price' })
|
||||
.populate({ path: 'course', select: 'title type price sectionCount hoursPerSection' })
|
||||
.populate({ path: 'professor', select: 'name surname' })
|
||||
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
||||
Class.countDocuments(filter)
|
||||
@@ -76,7 +84,7 @@ const getAll = async (query) => {
|
||||
|
||||
const getOne = async (id) => {
|
||||
const cls = await Class.findById(id)
|
||||
.populate({ path: 'course', select: 'title type price' })
|
||||
.populate({ path: 'course', select: 'title type price sectionCount hoursPerSection' })
|
||||
.populate({ path: 'professor', select: 'name surname phoneNumber' })
|
||||
.populate({ path: 'students', select: 'name phoneNumber gender' })
|
||||
.lean();
|
||||
@@ -88,6 +96,7 @@ const create = async (body) => {
|
||||
const payload = applyScheduleFields({
|
||||
...body,
|
||||
...normalizePricingFields(body),
|
||||
...normalizePayoutFields(body),
|
||||
numberOfSessions: normalizeNumberOfSessions(body.numberOfSessions)
|
||||
}, body);
|
||||
if (body.freeSpots === '' || body.freeSpots === null || body.freeSpots === undefined) {
|
||||
@@ -103,6 +112,7 @@ const update = async (id, body) => {
|
||||
const payload = applyScheduleFields({
|
||||
...body,
|
||||
...normalizePricingFields(body),
|
||||
...normalizePayoutFields(body),
|
||||
...(body.numberOfSessions !== undefined
|
||||
? { numberOfSessions: normalizeNumberOfSessions(body.numberOfSessions) }
|
||||
: {})
|
||||
|
||||
Reference in New Issue
Block a user