Files
gameno-api/utils/professorShare.js

142 lines
5.3 KiB
JavaScript

'use strict';
const { normalizeClockTime } = require('./classSchedule');
const PAYOUT_TYPES = {
PERCENTAGE: 'percentage',
HOURLY: 'hourly'
};
const toNonNegativeNumber = (value) => {
const n = Number(value);
if (!Number.isFinite(n) || n < 0) return 0;
return n;
};
/** Clamps a percentage-style value into the 0-100 range. */
const toPercentage = (value) => Math.min(100, toNonNegativeNumber(value));
/**
* Parses "HH:MM" clock strings and returns the duration between them, in hours.
* Returns 0 for missing/invalid input. Assumes the session does not cross midnight
* unless the end time is numerically before the start time (e.g. 23:00 -> 01:00).
*/
const calculateSessionDurationHours = (startTime, endTime) => {
const start = normalizeClockTime(startTime);
const end = normalizeClockTime(endTime);
if (!start || !end) return 0;
const [startH, startM] = start.split(':').map(Number);
const [endH, endM] = end.split(':').map(Number);
let diffMinutes = (endH * 60 + endM) - (startH * 60 + startM);
if (diffMinutes <= 0) diffMinutes += 24 * 60;
return diffMinutes / 60;
};
/**
* Resolves "Session Duration in Hours" for a class: prefers the linked course's
* `hoursPerSection`, falling back to the class's own start/end time window.
*/
const resolveSessionDurationHours = (cls = {}) => {
const course = cls.course && typeof cls.course === 'object' ? cls.course : null;
const hoursPerSection = toNonNegativeNumber(course?.hoursPerSection);
if (hoursPerSection > 0) return hoursPerSection;
return calculateSessionDurationHours(cls.startTime, cls.endTime);
};
/**
* Model A — percentage of class revenue.
* `revenue` is the amount the percentage should be applied to (e.g. actual received revenue).
* `serviceFeePerPerson` (catering / service expenses per student) is deducted first before calculating the professor share.
*/
const calculatePercentageShare = ({ payoutPercentage = 0, revenue = 0, serviceFeePerPerson = 0, studentsCount = 0 } = {}) => {
const totalServiceFee = toNonNegativeNumber(serviceFeePerPerson) * toNonNegativeNumber(studentsCount);
const netRevenue = Math.max(0, toNonNegativeNumber(revenue) - totalServiceFee);
return (toPercentage(payoutPercentage) / 100) * netRevenue;
};
/**
* Model B — hourly rate.
* Session Share = Hourly Rate * Session Duration in Hours
* Total Share = Session Share * Number of Sessions
*/
const calculateHourlyShare = ({ payoutHourlyRate = 0, sessionDurationHours = 0, sessionsCount = 0 } = {}) => {
const sessionShare = toNonNegativeNumber(payoutHourlyRate) * toNonNegativeNumber(sessionDurationHours);
return sessionShare * toNonNegativeNumber(sessionsCount);
};
/** Total Extra Expenses = extraExpensePerSession * Number of Sessions Held (always flat, per-session). */
const calculateExtraExpenses = ({ extraExpensePerSession = 0, sessionsCount = 0 } = {}) => {
return toNonNegativeNumber(extraExpensePerSession) * toNonNegativeNumber(sessionsCount);
};
/**
* Computes the professor's base share (before extra expenses) for either payout model.
*/
const calculateBaseShare = (params = {}) => {
const { payoutType } = params;
if (payoutType === PAYOUT_TYPES.HOURLY) {
return calculateHourlyShare(params);
}
return calculatePercentageShare(params);
};
/**
* Professor Total Payout = Base Share + (extraExpensePerSession * sessionsCount)
*/
const calculateProfessorPayout = (params = {}) => {
const baseShare = calculateBaseShare(params);
const extraExpenses = calculateExtraExpenses(params);
return {
baseShare,
extraExpenses,
totalPayout: baseShare + extraExpenses
};
};
/**
* Net Profit = Total Income - Professor Total Payout.
* Intentionally NOT clamped to zero — a negative result represents a real loss.
*/
const calculateNetProfit = ({ totalIncome = 0, professorTotalPayout = 0 } = {}) => {
return toNonNegativeNumber(totalIncome) - toNonNegativeNumber(professorTotalPayout);
};
/** Outstanding receivables can never be negative — overpayments are surfaced separately. */
const calculatePendingReceivables = (expectedRevenue = 0, actualReceivedRevenue = 0) => {
return Math.max(0, toNonNegativeNumber(expectedRevenue) - toNonNegativeNumber(actualReceivedRevenue));
};
/** Amount collected beyond what was actually owed (graceful handling of overpayments). */
const calculateOverpaidAmount = (expectedRevenue = 0, actualReceivedRevenue = 0) => {
return Math.max(0, toNonNegativeNumber(actualReceivedRevenue) - toNonNegativeNumber(expectedRevenue));
};
/**
* Per-session student revenue = Total Assigned Tuition per Student / Total Number of Planned Sessions.
* Guards against division by zero when a class has no planned sessions.
*/
const calculateStudentRevenuePerSession = (totalAssignedTuitionPerStudent = 0, totalPlannedSessions = 0) => {
const sessions = toNonNegativeNumber(totalPlannedSessions);
if (sessions <= 0) return 0;
return toNonNegativeNumber(totalAssignedTuitionPerStudent) / sessions;
};
module.exports = {
PAYOUT_TYPES,
toNonNegativeNumber,
toPercentage,
calculateSessionDurationHours,
resolveSessionDurationHours,
calculatePercentageShare,
calculateHourlyShare,
calculateExtraExpenses,
calculateBaseShare,
calculateProfessorPayout,
calculateNetProfit,
calculatePendingReceivables,
calculateOverpaidAmount,
calculateStudentRevenuePerSession
};