'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 };