Update staffing management to enhance payroll and hiring functionalities
- Increased the `MonthlyPayrollCap` from 10,000 to 40,000, allowing for greater flexibility in hiring and subject assignments. - Revised payroll calculation logic to ensure that staff members are compensated based on their actual weekly hours, with a minimum payment reflecting one full rate. - Updated documentation to clarify the new payroll structure and its implications for hiring and subject assignments. - Enhanced tests to validate the new payroll cap and ensure proper functionality in staffing scenarios, including the handling of uncovered subjects. - Improved localization strings to reflect changes in staffing and payroll terminology.
This commit is contained in:
@@ -4,16 +4,22 @@ public class StaffingTests
|
||||
{
|
||||
private const float Cap = 10_000f;
|
||||
|
||||
/// <summary>
|
||||
/// Price of an hour times the hours carried, with one full rate as the floor: a hired person
|
||||
/// costs something even idle, and loading them past the rate costs more. The number of
|
||||
/// subjects does not enter into it — twenty hours are twenty hours.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MonthlyPay_AddsSurchargeAfterTheFirstSubject()
|
||||
public void MonthlyPay_IsHoursTimesTheHourlyAsk_WithAFullRateAsTheFloor()
|
||||
{
|
||||
var rules = Fixtures.Catalog().StaffingRules!;
|
||||
|
||||
Assert.Equal(4_000f, Staffing.MonthlyBase(rules, 50f));
|
||||
Assert.Equal(4_000f, Staffing.MonthlyPay(rules, 50f, 0));
|
||||
Assert.Equal(4_000f, Staffing.MonthlyPay(rules, 50f, 1));
|
||||
Assert.Equal(5_000f, Staffing.MonthlyPay(rules, 50f, 2));
|
||||
Assert.Equal(6_000f, Staffing.MonthlyPay(rules, 50f, 3));
|
||||
Assert.Equal(4_000f, Staffing.MonthlyPay(rules, 50f, 0f));
|
||||
Assert.Equal(4_000f, Staffing.MonthlyPay(rules, 50f, 12f));
|
||||
Assert.Equal(4_000f, Staffing.MonthlyPay(rules, 50f, 20f));
|
||||
Assert.Equal(6_000f, Staffing.MonthlyPay(rules, 50f, 30f));
|
||||
Assert.Equal(7_200f, Staffing.MonthlyPay(rules, 50f, 36f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -53,25 +59,85 @@ public class StaffingTests
|
||||
var hired = Staffing.Hire(catalog, map, roster, pool, first.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.None, hired.Error);
|
||||
|
||||
// Mathematics is 35 curriculum hours a week, Literature 21. At 37.5 an hour that is
|
||||
// 37.5 × 56 × 4 = 8 400 a month for one person — and no room left for a second.
|
||||
var loaded = hired;
|
||||
foreach (var subject in new[] { "Mathematics", "RussianLanguage", "Literature", "History", "Biology", "ForeignLanguage" })
|
||||
foreach (var subject in new[] { "Mathematics", "Literature" })
|
||||
{
|
||||
loaded = Staffing.AssignSubject(catalog, loaded.Roster, loaded.Pool, first.Person.Id, subject, Cap);
|
||||
Assert.Equal(StaffingError.None, loaded.Error);
|
||||
}
|
||||
|
||||
Assert.Equal(6_750f, loaded.Payroll);
|
||||
Assert.Equal(8_400f, loaded.Payroll);
|
||||
|
||||
var blocked = Staffing.Hire(catalog, map, loaded.Roster, loaded.Pool, second.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.PayrollExceeded, blocked.Error);
|
||||
|
||||
var dropped = Staffing.UnassignSubject(catalog, loaded.Roster, loaded.Pool, first.Person.Id, "Biology", Cap);
|
||||
var dropped = Staffing.UnassignSubject(catalog, loaded.Roster, loaded.Pool, first.Person.Id, "Literature", Cap);
|
||||
Assert.Equal(StaffingError.None, dropped.Error);
|
||||
Assert.Equal(6_000f, dropped.Payroll);
|
||||
Assert.Equal(5_250f, dropped.Payroll);
|
||||
|
||||
var fitted = Staffing.Hire(catalog, map, dropped.Roster, dropped.Pool, second.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.None, fitted.Error);
|
||||
Assert.Equal(10_000f, fitted.Payroll);
|
||||
Assert.Equal(9_250f, fitted.Payroll);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two teachers of one subject split its hours, so the second one costs a full rate and makes
|
||||
/// the first cheaper. That is the whole reason hiring another is not simply worse than loading
|
||||
/// the one you have.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TwoTeachersOfASubject_ShareItsHours()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var first = WithAsk(pool.Applicants[0], 50f);
|
||||
var second = WithAsk(pool.Applicants[1], 50f);
|
||||
pool = pool with { Applicants = [first, second, .. pool.Applicants.Skip(2)] };
|
||||
|
||||
var one = Staffing.Hire(catalog, map, roster, pool, first.Person.Id, Staffing.TeacherPosition, 50_000f);
|
||||
one = Staffing.AssignSubject(catalog, one.Roster, one.Pool, first.Person.Id, "Mathematics", 50_000f);
|
||||
|
||||
// All 35 hours on one person: 50 × 35 × 4.
|
||||
Assert.Equal(7_000f, one.Payroll);
|
||||
|
||||
var two = Staffing.Hire(catalog, map, one.Roster, one.Pool, second.Person.Id, Staffing.TeacherPosition, 50_000f);
|
||||
two = Staffing.AssignSubject(catalog, two.Roster, two.Pool, second.Person.Id, "Mathematics", 50_000f);
|
||||
|
||||
// 17.5 hours each, which is under a full rate, so both are paid the rate: 4 000 × 2.
|
||||
Assert.Equal(8_000f, two.Payroll);
|
||||
Assert.All(
|
||||
two.Roster.People.Where(person => person.IsStaff),
|
||||
person => Assert.Equal(17.5f, Staffing.WeeklyHours(catalog, two.Roster, person)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primary school is twenty hours a week for each of four classes. One teacher cannot carry
|
||||
/// eighty hours whatever the budget says, so the subject stays on the uncovered list until
|
||||
/// enough people share it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SubjectNeedingMoreHoursThanOnePersonCarries_StaysUncovered()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var rules = catalog.StaffingRules!;
|
||||
Assert.Equal(80f, Staffing.SubjectHours(catalog, roster, "PrimarySchool"));
|
||||
Assert.True(80f > rules.MaxWeeklyHours);
|
||||
|
||||
var outcome = new StaffingOutcome(StaffingError.None, roster, pool, 0, 0, 0, 0);
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var applicant = outcome.Pool.Applicants[0];
|
||||
outcome = Staffing.Hire(catalog, map, outcome.Roster, outcome.Pool, applicant.Person.Id, Staffing.TeacherPosition, 500_000f);
|
||||
Assert.Equal(StaffingError.None, outcome.Error);
|
||||
outcome = Staffing.AssignSubject(catalog, outcome.Roster, outcome.Pool, applicant.Person.Id, "PrimarySchool", 500_000f);
|
||||
Assert.Equal(StaffingError.None, outcome.Error);
|
||||
|
||||
var stillShort = 80f > (i + 1) * rules.MaxWeeklyHours;
|
||||
Assert.Equal(
|
||||
stillShort,
|
||||
Staffing.Uncovered(catalog, outcome.Roster).Any(subject => subject.DefName == "PrimarySchool"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -82,23 +148,22 @@ public class StaffingTests
|
||||
pool = pool with { Applicants = [applicant, .. pool.Applicants.Skip(1)] };
|
||||
|
||||
var hired = Staffing.Hire(catalog, map, roster, pool, applicant.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
var loaded = hired;
|
||||
foreach (var subject in new[] { "Mathematics", "RussianLanguage", "Literature", "History", "Biology", "Physics", "Chemistry" })
|
||||
{
|
||||
loaded = Staffing.AssignSubject(catalog, loaded.Roster, loaded.Pool, applicant.Person.Id, subject, Cap);
|
||||
Assert.Equal(StaffingError.None, loaded.Error);
|
||||
}
|
||||
Assert.Equal(4_000f, hired.Payroll);
|
||||
|
||||
Assert.Equal(10_000f, loaded.Payroll);
|
||||
// Mathematics is 35 hours: 50 × 35 × 4 = 7 000.
|
||||
var loaded = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, applicant.Person.Id, "Mathematics", Cap);
|
||||
Assert.Equal(StaffingError.None, loaded.Error);
|
||||
Assert.Equal(7_000f, loaded.Payroll);
|
||||
|
||||
var rejected = Staffing.AssignSubject(catalog, loaded.Roster, loaded.Pool, applicant.Person.Id, "Geography", Cap);
|
||||
// Russian adds 28 more. 63 hours at 50 is 12 600 a month — past the 10 000 allocation.
|
||||
var rejected = Staffing.AssignSubject(catalog, loaded.Roster, loaded.Pool, applicant.Person.Id, "RussianLanguage", Cap);
|
||||
Assert.Equal(StaffingError.PayrollExceeded, rejected.Error);
|
||||
Assert.Equal(10_000f, rejected.Payroll);
|
||||
Assert.Equal(11_000f, rejected.Attempted);
|
||||
Assert.Equal(0f, rejected.Remaining);
|
||||
Assert.Equal(7_000f, rejected.Payroll);
|
||||
Assert.Equal(12_600f, rejected.Attempted);
|
||||
Assert.Equal(3_000f, rejected.Remaining);
|
||||
Assert.DoesNotContain(
|
||||
rejected.Roster.People.Single(person => person.Id == applicant.Person.Id).Subjects,
|
||||
name => name == "Geography");
|
||||
name => name == "RussianLanguage");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user