Enhance staffing management in school simulation by introducing API endpoints for hiring staff and assigning subjects. Implement payroll cap validation to ensure hiring and subject assignments do not exceed the allocated budget. Update the simulation options to include a monthly payroll cap and revise related classes to support new staffing functionalities. Enhance documentation to reflect these changes and update tests to validate the new features.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
namespace HSchool.People.Tests;
|
||||
|
||||
public class StaffingTests
|
||||
{
|
||||
private const float Cap = 10_000f;
|
||||
|
||||
[Fact]
|
||||
public void MonthlyPay_AddsSurchargeAfterTheFirstSubject()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hire_TwoPeopleAtThreeAndSevenThousand_RejectsTheThird()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var cheap = WithAsk(pool.Applicants[0], 37.5f);
|
||||
var expensive = WithAsk(pool.Applicants[1], 87.5f);
|
||||
var extra = WithAsk(pool.Applicants[2], 40f);
|
||||
pool = pool with { Applicants = [cheap, expensive, extra] };
|
||||
|
||||
var first = Staffing.Hire(catalog, map, roster, pool, cheap.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.None, first.Error);
|
||||
Assert.Equal(3_000f, first.Payroll);
|
||||
|
||||
var second = Staffing.Hire(catalog, map, first.Roster, first.Pool, expensive.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.None, second.Error);
|
||||
Assert.Equal(10_000f, second.Payroll);
|
||||
Assert.Equal(0f, second.Remaining);
|
||||
|
||||
var third = Staffing.Hire(catalog, map, second.Roster, second.Pool, extra.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.PayrollExceeded, third.Error);
|
||||
Assert.Equal(10_000f, third.Payroll);
|
||||
Assert.True(third.Attempted > Cap);
|
||||
Assert.Equal(2, second.Roster.People.Count(person => person.IsStaff));
|
||||
Assert.DoesNotContain(third.Roster.People, person => person.Id == extra.Person.Id && person.IsStaff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnassignSubject_LowersPayrollSoAnotherHireFits()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var first = WithAsk(pool.Applicants[0], 37.5f);
|
||||
var second = WithAsk(pool.Applicants[1], 50f);
|
||||
pool = pool with { Applicants = [first, second, .. pool.Applicants.Skip(2)] };
|
||||
|
||||
var hired = Staffing.Hire(catalog, map, roster, pool, first.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
Assert.Equal(StaffingError.None, hired.Error);
|
||||
|
||||
var loaded = hired;
|
||||
foreach (var subject in new[] { "Mathematics", "RussianLanguage", "Literature", "History", "Biology", "ForeignLanguage" })
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
Assert.Equal(StaffingError.None, dropped.Error);
|
||||
Assert.Equal(6_000f, 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignSubject_ThatWouldExceedTheCap_IsRejected()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var applicant = WithAsk(pool.Applicants[0], 50f);
|
||||
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(10_000f, loaded.Payroll);
|
||||
|
||||
var rejected = Staffing.AssignSubject(catalog, loaded.Roster, loaded.Pool, applicant.Person.Id, "Geography", 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.DoesNotContain(
|
||||
rejected.Roster.People.Single(person => person.Id == applicant.Person.Id).Subjects,
|
||||
name => name == "Geography");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hire_ParentKeepsTheSameId_GeneratedJoinsTheRoster()
|
||||
{
|
||||
var catalog = Fixtures.Catalog();
|
||||
var map = Fixtures.VanillaMap();
|
||||
Roster? roster = null;
|
||||
Applicant? parent = null;
|
||||
Applicant? generated = null;
|
||||
ApplicantPool? pool = null;
|
||||
for (var seed = 1; seed <= 30 && pool is null; seed++)
|
||||
{
|
||||
var candidate = Fixtures.Generate(map, seed);
|
||||
var created = ApplicantPool.Create(catalog, candidate, seed, "Slavic", Fixtures.AsOf);
|
||||
var rosterIds = candidate.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var parentHere = created.Applicants.FirstOrDefault(applicant => rosterIds.Contains(applicant.Person.Id));
|
||||
var generatedHere = created.Applicants.FirstOrDefault(applicant => applicant.Person.Id.StartsWith('a'));
|
||||
if (parentHere is not null && generatedHere is not null)
|
||||
{
|
||||
roster = candidate;
|
||||
pool = created;
|
||||
parent = parentHere;
|
||||
generated = generatedHere;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.NotNull(roster);
|
||||
Assert.NotNull(pool);
|
||||
Assert.NotNull(parent);
|
||||
Assert.NotNull(generated);
|
||||
|
||||
var afterParent = Staffing.Hire(catalog, map, roster, pool, parent.Person.Id, Staffing.TeacherPosition, 50_000f);
|
||||
Assert.Equal(StaffingError.None, afterParent.Error);
|
||||
var staffParent = afterParent.Roster.People.Single(person => person.Id == parent.Person.Id);
|
||||
Assert.True(staffParent.IsParent);
|
||||
Assert.True(staffParent.IsStaff);
|
||||
Assert.Equal(1, afterParent.Roster.People.Count(person => person.Id == parent.Person.Id));
|
||||
Assert.DoesNotContain(afterParent.Pool.Applicants, applicant => applicant.Person.Id == parent.Person.Id);
|
||||
|
||||
var afterGenerated = Staffing.Hire(catalog, map, afterParent.Roster, afterParent.Pool, generated.Person.Id, Staffing.TeacherPosition, 50_000f);
|
||||
Assert.Equal(StaffingError.None, afterGenerated.Error);
|
||||
Assert.Contains(afterGenerated.Roster.People, person => person.Id == generated.Person.Id && person.IsStaff && !person.IsParent);
|
||||
Assert.Contains(afterGenerated.Roster.Families, family => family.Id == generated.Person.FamilyId);
|
||||
|
||||
var again = Staffing.Hire(catalog, map, afterGenerated.Roster, afterGenerated.Pool, parent.Person.Id, Staffing.TeacherPosition, 50_000f);
|
||||
Assert.Equal(StaffingError.AlreadyHired, again.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Uncovered_ListsSubjectsTaughtInExistingYearsWithoutATeacher()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var uncovered = Staffing.Uncovered(catalog, roster);
|
||||
Assert.Contains(uncovered, subject => subject.DefName == "PrimarySchool");
|
||||
Assert.Contains(uncovered, subject => subject.DefName == "Mathematics");
|
||||
Assert.Contains(uncovered, subject => subject.DefName == "PhysicalEducation");
|
||||
Assert.Equal(catalog.Subjects.Values.Count(subject => !subject.Abstract), uncovered.Count);
|
||||
|
||||
var applicant = pool.Applicants[0];
|
||||
var hired = Staffing.Hire(catalog, map, roster, pool, applicant.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
var assigned = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, applicant.Person.Id, "Mathematics", Cap);
|
||||
|
||||
Assert.DoesNotContain(Staffing.Uncovered(catalog, assigned.Roster), subject => subject.DefName == "Mathematics");
|
||||
Assert.Contains(Staffing.Uncovered(catalog, assigned.Roster), subject => subject.DefName == "PrimarySchool");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeopleJson_RoundTripsHiredStaffAndSubjects()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
var applicant = pool.Applicants[0];
|
||||
var hired = Staffing.Hire(catalog, map, roster, pool, applicant.Person.Id, Staffing.TeacherPosition, Cap);
|
||||
var assigned = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, applicant.Person.Id, "Mathematics", Cap);
|
||||
|
||||
var json = RosterJson.Serialize(RosterDocument.From(Fixtures.SchoolSeed, assigned.Roster, assigned.Pool));
|
||||
var loaded = RosterJson.Parse(json);
|
||||
var person = loaded.People.Single(member => member.Id == applicant.Person.Id);
|
||||
|
||||
Assert.True(person.IsStaff);
|
||||
Assert.Equal(Staffing.TeacherPosition, person.Position);
|
||||
Assert.Equal(applicant.HourlyWageAsk, person.HourlyWageAsk);
|
||||
Assert.Equal(["Mathematics"], person.Subjects);
|
||||
Assert.DoesNotContain(loaded.Applicants!.Applicants, candidate => candidate.Person.Id == applicant.Person.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hire_UnknownApplicantAndUnknownPosition()
|
||||
{
|
||||
var (catalog, map, roster, pool) = Fresh();
|
||||
|
||||
Assert.Equal(
|
||||
StaffingError.UnknownApplicant,
|
||||
Staffing.Hire(catalog, map, roster, pool, "nobody", Staffing.TeacherPosition, Cap).Error);
|
||||
Assert.Equal(
|
||||
StaffingError.UnknownPosition,
|
||||
Staffing.Hire(catalog, map, roster, pool, pool.Applicants[0].Person.Id, "Janitor", Cap).Error);
|
||||
}
|
||||
|
||||
private static (DefCatalog Catalog, MapLayout Map, Roster Roster, ApplicantPool Pool) Fresh()
|
||||
{
|
||||
var catalog = Fixtures.Catalog();
|
||||
var map = Fixtures.VanillaMap();
|
||||
var roster = Fixtures.Generate(map);
|
||||
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf);
|
||||
return (catalog, map, roster, pool);
|
||||
}
|
||||
|
||||
private static Applicant WithAsk(Applicant applicant, float hourlyAsk) =>
|
||||
applicant with { HourlyWageAsk = hourlyAsk };
|
||||
}
|
||||
Reference in New Issue
Block a user