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,260 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class StaffingApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task GetStaffing_UnknownSchool_IsNotFound()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
using var response = await client.GetAsync("/api/schools/999999/staffing", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal("unknown-school", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStaffing_EmptySchool_HasPoolAndNoPayroll()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Штат пустой", Start);
|
||||
|
||||
var staffing = await GetStaffingAsync(client, school.Id);
|
||||
|
||||
Assert.Equal(10_000f, staffing.Allocated);
|
||||
Assert.Equal(0f, staffing.Payroll);
|
||||
Assert.Equal(10_000f, staffing.Remaining);
|
||||
Assert.Equal(12, staffing.Applicants.Count);
|
||||
Assert.Empty(staffing.Staff);
|
||||
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "Mathematics");
|
||||
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "PrimarySchool");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hire_AddsStaff_RemovesApplicant_AndRejectsASecondHireOfTheSamePerson()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Штат наём", Start);
|
||||
var before = await GetStaffingAsync(client, school.Id);
|
||||
var applicant = before.Applicants[0];
|
||||
|
||||
var after = await HireAsync(client, school.Id, applicant.Id, "Teacher");
|
||||
|
||||
Assert.Contains(after.Staff, member => member.Id == applicant.Id && member.Position == "Teacher");
|
||||
Assert.DoesNotContain(after.Applicants, candidate => candidate.Id == applicant.Id);
|
||||
Assert.True(after.Payroll > 0f);
|
||||
Assert.True(after.Payroll <= after.Allocated);
|
||||
|
||||
using var again = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = applicant.Id, position = "Teacher" },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
|
||||
Assert.Equal("already-hired", await ProblemCodeAsync(again));
|
||||
|
||||
var people = await client.GetFromJsonAsync<PeopleListResponse>(
|
||||
$"/api/schools/{school.Id}/people?role=staff&pageSize=10",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(people);
|
||||
Assert.Contains(people.People, person => person.Id == applicant.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hire_UnknownApplicant_IsNotFound()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Штат неизвестный", Start);
|
||||
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = "nobody", position = "Teacher" },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal("unknown-applicant", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hire_PastTheCap_IsRejectedWithTheNumbers()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Штат предел", Start);
|
||||
|
||||
var staffing = await GetStaffingAsync(client, school.Id);
|
||||
ProblemResponse? problem = null;
|
||||
while (staffing.Applicants.Count > 0)
|
||||
{
|
||||
var applicant = staffing.Applicants[0];
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = applicant.Id, position = "Teacher" },
|
||||
TestContext.Current.CancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
}
|
||||
|
||||
Assert.Equal("payroll-exceeded", problem?.Code);
|
||||
Assert.Equal(10_000f, problem?.Allocated);
|
||||
Assert.True(problem?.Payroll <= 10_000f);
|
||||
Assert.True(problem?.Attempted > 10_000f);
|
||||
Assert.True(staffing.Staff.Count >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Штат предметы", Start);
|
||||
var before = await GetStaffingAsync(client, school.Id);
|
||||
var hired = await HireAsync(client, school.Id, before.Applicants[0].Id, "Teacher");
|
||||
var teacher = hired.Staff.Single();
|
||||
|
||||
var assigned = await AssignAsync(client, school.Id, teacher.Id, "Mathematics");
|
||||
Assert.Equal(hired.Payroll, assigned.Payroll);
|
||||
|
||||
assigned = await AssignAsync(client, school.Id, teacher.Id, "RussianLanguage");
|
||||
Assert.True(assigned.Payroll > hired.Payroll);
|
||||
Assert.Equal(2, assigned.Staff.Single().Subjects.Count);
|
||||
|
||||
using var over = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(teacher.Id)}/subjects",
|
||||
new { subject = "Literature" },
|
||||
TestContext.Current.CancellationToken);
|
||||
// Cheap asks may still fit; only the numbers matter when the cap actually bites.
|
||||
if (over.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
var problem = await over.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.Equal("payroll-exceeded", problem?.Code);
|
||||
Assert.True(problem?.Attempted > problem?.Allocated);
|
||||
Assert.Equal(assigned.Payroll, problem?.Payroll);
|
||||
}
|
||||
else
|
||||
{
|
||||
over.EnsureSuccessStatusCode();
|
||||
assigned = await over.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(assigned);
|
||||
}
|
||||
|
||||
var payrollWithSubjects = assigned.Payroll;
|
||||
using var unassign = await client.DeleteAsync(
|
||||
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(teacher.Id)}/subjects/RussianLanguage",
|
||||
TestContext.Current.CancellationToken);
|
||||
unassign.EnsureSuccessStatusCode();
|
||||
var afterDrop = await unassign.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(afterDrop);
|
||||
Assert.True(afterDrop.Payroll < payrollWithSubjects);
|
||||
|
||||
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||
reload.EnsureSuccessStatusCode();
|
||||
|
||||
var restored = await GetStaffingAsync(client, school.Id);
|
||||
Assert.Equal(afterDrop.Payroll, restored.Payroll);
|
||||
Assert.Equal(teacher.Id, restored.Staff.Single().Id);
|
||||
Assert.Equal(
|
||||
afterDrop.Staff.Single().Subjects.Select(subject => subject.DefName),
|
||||
restored.Staff.Single().Subjects.Select(subject => subject.DefName));
|
||||
}
|
||||
|
||||
private static async Task<StaffingResponse> GetStaffingAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
|
||||
$"/api/schools/{schoolId}/staffing?lang=ru",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
return staffing;
|
||||
}
|
||||
|
||||
private static async Task<StaffingResponse> HireAsync(HttpClient client, int schoolId, string personId, string position)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{schoolId}/staff/hire",
|
||||
new { personId, position },
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
return staffing;
|
||||
}
|
||||
|
||||
private static async Task<StaffingResponse> AssignAsync(HttpClient client, int schoolId, string personId, string subject)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{schoolId}/staff/{Uri.EscapeDataString(personId)}/subjects",
|
||||
new { subject },
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
return staffing;
|
||||
}
|
||||
|
||||
private static async Task<string?> ProblemCodeAsync(HttpResponseMessage response)
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
return problem?.Code;
|
||||
}
|
||||
|
||||
private sealed record ProblemResponse(string? Code, float? Allocated, float? Payroll, float? Remaining, float? Attempted);
|
||||
|
||||
private sealed record StaffingResponse(
|
||||
float Allocated,
|
||||
float Payroll,
|
||||
float Remaining,
|
||||
IReadOnlyList<UncoveredSubjectResponse> Uncovered,
|
||||
IReadOnlyList<ApplicantResponse> Applicants,
|
||||
IReadOnlyList<StaffMemberResponse> Staff);
|
||||
|
||||
private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek);
|
||||
|
||||
private sealed record ApplicantResponse(string Id, string FullName, bool Female, int Age, bool IsParent, float HourlyWageAsk, float MonthlyBase);
|
||||
|
||||
private sealed record StaffMemberResponse(
|
||||
string Id,
|
||||
string FullName,
|
||||
bool Female,
|
||||
int Age,
|
||||
bool IsParent,
|
||||
string Position,
|
||||
string PositionLabel,
|
||||
float HourlyWageAsk,
|
||||
float MonthlyPay,
|
||||
IReadOnlyList<DefLabelResponse> Subjects);
|
||||
|
||||
private sealed record DefLabelResponse(string DefName, string Label);
|
||||
|
||||
private sealed record PeopleListResponse(
|
||||
int Total,
|
||||
int Page,
|
||||
int PageSize,
|
||||
IReadOnlyList<PersonListItemResponse> People,
|
||||
object Filters);
|
||||
|
||||
private sealed record PersonListItemResponse(
|
||||
string Id,
|
||||
string FullName,
|
||||
string Surname,
|
||||
string Given,
|
||||
string Patronymic,
|
||||
bool Female,
|
||||
int Age,
|
||||
IReadOnlyList<string> Roles,
|
||||
int? ClassYear,
|
||||
string? ClassLetter,
|
||||
string? Position,
|
||||
string? PositionLabel);
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class PeopleDefTests
|
||||
Assert.True(catalog.Subjects.ContainsKey("PrimarySchool"));
|
||||
Assert.NotNull(catalog.StaffingRules);
|
||||
Assert.Equal(12, catalog.StaffingRules.PoolSize);
|
||||
Assert.Equal(20, catalog.StaffingRules.BaseWeeklyHours);
|
||||
Assert.Equal(4, catalog.StaffingRules.WeeksPerMonth);
|
||||
Assert.Equal(0.25f, catalog.StaffingRules.ExtraSubjectSurcharge);
|
||||
Assert.Equal("Начальные классы", catalog.Label("ru", catalog.Subjects["PrimarySchool"]));
|
||||
Assert.Equal("Primary", catalog.Label("en", catalog.Subjects["PrimarySchool"]));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ public class VanillaCoreTests
|
||||
Assert.True(catalog.Subjects.ContainsKey("PhysicalEducation"));
|
||||
Assert.NotNull(catalog.StaffingRules);
|
||||
Assert.Equal(12, catalog.StaffingRules.PoolSize);
|
||||
Assert.Equal(20, catalog.StaffingRules.BaseWeeklyHours);
|
||||
Assert.Equal(0.25f, catalog.StaffingRules.ExtraSubjectSurcharge);
|
||||
Assert.Equal(2, map.Buildings.Count);
|
||||
var homerooms = map.Rooms.Where(room => room.Def == "Classroom").ToList();
|
||||
Assert.Equal(11, homerooms.Count);
|
||||
|
||||
@@ -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