Add professor share & financial reports UI, expense management page
Adds payout configuration fields (percentage/hourly + per-session expense allowance) with a live payout preview to the class form, a new Financial Reports page (per-class and date-range/monthly tabs, per-session drill-down dialog) with reusable stat cards, and an Institutional Expenses management page. Registers routes, sidebar navigation, and permission constants for the new financial_reports and expenses permissions.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
calculateSessionDurationHours,
|
||||
resolveSessionDurationHours,
|
||||
calculatePercentageShare,
|
||||
calculateHourlyShare,
|
||||
calculateExtraExpenses,
|
||||
calculateProfessorPayout,
|
||||
calculateNetProfit,
|
||||
calculatePendingReceivables,
|
||||
calculateOverpaidAmount
|
||||
} from './professorShare.js';
|
||||
|
||||
describe('calculateSessionDurationHours', () => {
|
||||
it('computes duration between two clock times', () => {
|
||||
assert.equal(calculateSessionDurationHours('18:00', '20:00'), 2);
|
||||
});
|
||||
|
||||
it('returns 0 for invalid input', () => {
|
||||
assert.equal(calculateSessionDurationHours('', ''), 0);
|
||||
assert.equal(calculateSessionDurationHours(null, undefined), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveSessionDurationHours', () => {
|
||||
it('prefers hoursPerSection over the class time window', () => {
|
||||
assert.equal(resolveSessionDurationHours({ hoursPerSection: 2, startTime: '18:00', endTime: '20:30' }), 2);
|
||||
});
|
||||
|
||||
it('falls back to the class time window', () => {
|
||||
assert.equal(resolveSessionDurationHours({ startTime: '18:00', endTime: '19:30' }), 1.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateProfessorPayout', () => {
|
||||
it('computes percentage-based payout with extra expenses', () => {
|
||||
const result = calculateProfessorPayout({
|
||||
payoutType: 'percentage',
|
||||
payoutPercentage: 40,
|
||||
revenue: 10_000_000,
|
||||
extraExpensePerSession: 100_000,
|
||||
sessionsCount: 5
|
||||
});
|
||||
assert.equal(result.baseShare, 4_000_000);
|
||||
assert.equal(result.extraExpenses, 500_000);
|
||||
assert.equal(result.totalPayout, 4_500_000);
|
||||
});
|
||||
|
||||
it('computes hourly-based payout with extra expenses', () => {
|
||||
const result = calculateProfessorPayout({
|
||||
payoutType: 'hourly',
|
||||
payoutHourlyRate: 300_000,
|
||||
sessionDurationHours: 1.5,
|
||||
extraExpensePerSession: 80_000,
|
||||
sessionsCount: 10
|
||||
});
|
||||
assert.equal(result.baseShare, 4_500_000);
|
||||
assert.equal(result.extraExpenses, 800_000);
|
||||
assert.equal(result.totalPayout, 5_300_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateNetProfit', () => {
|
||||
it('can go negative to represent a loss', () => {
|
||||
assert.equal(calculateNetProfit({ totalIncome: 1_000_000, professorTotalPayout: 4_500_000 }), -3_500_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculatePendingReceivables / calculateOverpaidAmount', () => {
|
||||
it('never returns negative outstanding, and surfaces overpayments separately', () => {
|
||||
assert.equal(calculatePendingReceivables(10_000_000, 12_000_000), 0);
|
||||
assert.equal(calculateOverpaidAmount(10_000_000, 12_000_000), 2_000_000);
|
||||
});
|
||||
|
||||
it('handles zero revenue classes gracefully', () => {
|
||||
assert.equal(calculatePendingReceivables(0, 0), 0);
|
||||
assert.equal(calculateExtraExpenses({}), 0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user