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:
2026-08-21 12:19:09 +03:30
parent 3c6eb278b0
commit 22b57eeae2
20 changed files with 1095 additions and 6 deletions
+25
View File
@@ -79,6 +79,31 @@ const classSchema = new mongoose.Schema({
min: 1,
default: null
},
/** Professor compensation model for this class */
payoutType: {
type: String,
enum: ['percentage', 'hourly'],
default: 'percentage'
},
/** Used when payoutType === 'percentage' — professor's share of class revenue (0-100) */
payoutPercentage: {
type: Number,
default: 0,
min: 0,
max: 100
},
/** Used when payoutType === 'hourly' — professor's rate per teaching hour (Toman) */
payoutHourlyRate: {
type: Number,
default: 0,
min: 0
},
/** Optional flat allowance (travel/catering/etc.) paid per session held, regardless of payout model */
extraExpensePerSession: {
type: Number,
default: 0,
min: 0
},
isActive: {
type: Boolean,
default: true
+13 -3
View File
@@ -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) }
: {})