diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddaaf9b8..2a980f494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased][unreleased] +### Added + +- Added Governance turnover to governance page + ### Changed - Updated the node version in the github runners and docker image diff --git a/DfE.FindInformationAcademiesTrusts.Data/DateTimeProvider.cs b/DfE.FindInformationAcademiesTrusts.Data/DateTimeProvider.cs index ff0ebcc14..748d1ea8e 100644 --- a/DfE.FindInformationAcademiesTrusts.Data/DateTimeProvider.cs +++ b/DfE.FindInformationAcademiesTrusts.Data/DateTimeProvider.cs @@ -3,10 +3,12 @@ public interface IDateTimeProvider { DateTime Now { get; } + DateTime Today { get; } } public class DateTimeProvider : IDateTimeProvider { public DateTime Now => DateTime.Now; + public DateTime Today => DateTime.Today; } -} +} \ No newline at end of file diff --git a/DfE.FindInformationAcademiesTrusts.Data/Repositories/Trust/TrustGovernance.cs b/DfE.FindInformationAcademiesTrusts.Data/Repositories/Trust/TrustGovernance.cs index bde73c115..485a58a69 100644 --- a/DfE.FindInformationAcademiesTrusts.Data/Repositories/Trust/TrustGovernance.cs +++ b/DfE.FindInformationAcademiesTrusts.Data/Repositories/Trust/TrustGovernance.cs @@ -1,7 +1,7 @@ namespace DfE.FindInformationAcademiesTrusts.Data.Repositories.Trust; public record TrustGovernance( - Governor[] TrustLeadership, - Governor[] Members, - Governor[] Trustees, + Governor[] CurrentTrustLeadership, + Governor[] CurrentMembers, + Governor[] CurrentTrustees, Governor[] HistoricMembers); diff --git a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml index e6419804e..de45cf6a8 100644 --- a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml +++ b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml @@ -5,171 +5,189 @@ @{ Layout = "_TrustLayout"; } +
+
+
+
+

Governance turnover

+
+
+

+ @Model.TrustGovernance.TurnoverRate.ToString("0.#")% within the last 12 months +

+

Governance turnover % is calculated based on the total number of appointments and resignations in the past calendar year, divided by the total number of current governors.

+
+
+
+
+
+
+
+ @if (Model.TrustGovernance.CurrentTrustLeadership.Length > 0) + { + + + + + + + + + + + + @foreach (var governor in Model.TrustGovernance.CurrentTrustLeadership) + { + + + + + + + } + +
Trust Leadership
NameRoleFromTo
+ @governor.FullName + + @governor.Role + + @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() + + @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() +
+ } + else + { +

Trust Leadership

+

No Trust Leadership

+ } +
-
- @if (Model.TrustGovernance.TrustLeadership.Length > 0) - { - - - - - - - - - - - - @foreach (var governor in Model.TrustGovernance.TrustLeadership) - { - - - - - - - } - -
Trust Leadership
NameRoleFromTo
- @governor.FullName - - @governor.Role - - @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() - - @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() -
- } - else - { -

Trust Leadership

-

No Trust Leadership

- } -
+
+ @if (Model.TrustGovernance.CurrentTrustees.Length > 0) + { + + + + + + + + + + + + @foreach (var governor in Model.TrustGovernance.CurrentTrustees) + { + + + + + + + } + +
Trustees
NameAppointed byFromTo
+ @governor.FullName + + @governor.AppointingBody + + @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() + + @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() +
+ } + else + { +

Trustees

+

No Trustees

+ } +
-
- @if (Model.TrustGovernance.Trustees.Length > 0) - { - - - - - - - - - - - - @foreach (var governor in Model.TrustGovernance.Trustees) - { - - - - - - - } - -
Trustees
NameAppointed byFromTo
- @governor.FullName - - @governor.AppointingBody - - @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() - - @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() -
- } - else - { -

Trustees

-

No Trustees

- } -
+
+ @if (Model.TrustGovernance.CurrentMembers.Length > 0) + { + + + + + + + + + + + + @foreach (var governor in Model.TrustGovernance.CurrentMembers) + { + + + + + + + } + +
Members
NameAppointed byFromTo
+ @governor.FullName + + @governor.AppointingBody + + @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() + + @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() +
+ } + else + { +

Members

+

No Members

+ } +
-
- @if (Model.TrustGovernance.Members.Length > 0) - { - - - - - - - - - - - - @foreach (var governor in Model.TrustGovernance.Members) - { - - - - - - - } - -
Members
NameAppointed byFromTo
- @governor.FullName - - @governor.AppointingBody - - @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() - - @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() -
- } - else - { -

Members

-

No Members

- } -
- -
- @if (Model.TrustGovernance.HistoricMembers.Length > 0) - { - - - - - - - - - - - - - @foreach (var governor in Model.TrustGovernance.HistoricMembers) - { - - - - - - - - } - -
Historic members
NameRoleAppointed byFromTo
- @governor.FullName - - @governor.Role - - @governor.AppointingBody - - @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() - - @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() -
- } - else - { -

Historic members

-

No Historic members

- } -
+
+ @if (Model.TrustGovernance.HistoricMembers.Length > 0) + { + + + + + + + + + + + + + @foreach (var governor in Model.TrustGovernance.HistoricMembers) + { + + + + + + + + } + +
Historic members
NameRoleAppointed byFromTo
+ @governor.FullName + + @governor.Role + + @governor.AppointingBody + + @governor.DateOfAppointment.ShowDateStringOrReplaceWithText() + + @governor.DateOfTermEnd.ShowDateStringOrReplaceWithText() +
+ } + else + { +

Historic members

+

No Historic members

+ } +
+
+
\ No newline at end of file diff --git a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustGovernanceServiceModel.cs b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustGovernanceServiceModel.cs index 951e1d963..b25ed78c8 100644 --- a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustGovernanceServiceModel.cs +++ b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustGovernanceServiceModel.cs @@ -1,9 +1,12 @@ using DfE.FindInformationAcademiesTrusts.Data.Repositories.Trust; +using System.Diagnostics.CodeAnalysis; namespace DfE.FindInformationAcademiesTrusts.Services.Trust; +[ExcludeFromCodeCoverage] public record TrustGovernanceServiceModel( - Governor[] TrustLeadership, - Governor[] Members, - Governor[] Trustees, - Governor[] HistoricMembers); + Governor[] CurrentTrustLeadership, + Governor[] CurrentMembers, + Governor[] CurrentTrustees, + Governor[] HistoricMembers, + decimal TurnoverRate); \ No newline at end of file diff --git a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs index 77617ac61..085a94551 100644 --- a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs +++ b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs @@ -1,3 +1,4 @@ +using DfE.FindInformationAcademiesTrusts.Data; using DfE.FindInformationAcademiesTrusts.Data.Enums; using DfE.FindInformationAcademiesTrusts.Data.FiatDb.Repositories; using DfE.FindInformationAcademiesTrusts.Data.Repositories.Academy; @@ -21,7 +22,8 @@ public class TrustService( IAcademyRepository academyRepository, ITrustRepository trustRepository, IContactRepository contactRepository, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IDateTimeProvider dateTimeProvider) : ITrustService { public async Task GetTrustSummaryAsync(string uid) @@ -56,11 +58,14 @@ public async Task GetTrustGovernanceAsync(string ui var trustGovernance = await trustRepository.GetTrustGovernanceAsync(uid, urn); + var governanceTurnover = GetGovernanceTurnoverRate(trustGovernance); + return new TrustGovernanceServiceModel( - trustGovernance.TrustLeadership, - trustGovernance.Members, - trustGovernance.Trustees, - trustGovernance.HistoricMembers); + trustGovernance.CurrentTrustLeadership, + trustGovernance.CurrentMembers, + trustGovernance.CurrentTrustees, + trustGovernance.HistoricMembers, + governanceTurnover); } public async Task GetTrustContactsAsync(string uid) @@ -131,4 +136,72 @@ public async Task GetTrustOverviewAsync(string uid) return overviewModel; } + public decimal GetGovernanceTurnoverRate(TrustGovernance trustGovernance) + { + var today = dateTimeProvider.Today; + + // Past 12 Months + var past12MonthsStart = today.AddYears(-1); + + // Get current governors (Trustees and Members) + List currentGovernors = GetCurrentGovernors(trustGovernance); + + + // Get all governors for event calculations (including HistoricMembers), excluding specified roles + List eligibleGovernorsForTurnoverCalculation = GetGovernorsExcludingLeadership(trustGovernance); + + // Total number of current governor positions + int totalCurrentGovernors = currentGovernors.Count; + + // Appointments in the past 12 months + int appointmentsInPast12Months = CountEventsWithinDateRange( + eligibleGovernorsForTurnoverCalculation, + g => g.DateOfAppointment, + past12MonthsStart, + today + ); + + // Resignations in the past 12 months + int resignationsInPast12Months = CountEventsWithinDateRange( + eligibleGovernorsForTurnoverCalculation, + g => g.DateOfTermEnd, + past12MonthsStart, + today + ); + + int totalEvents = appointmentsInPast12Months + resignationsInPast12Months; + return CalculateTurnoverRate(totalCurrentGovernors, totalEvents); + } + + public static decimal CalculateTurnoverRate(int totalCurrentGovernors, int totalEvents) + { + // Calculate turnover rate and round to 1 decimal point + return totalCurrentGovernors > 0 + ? Math.Round((decimal)totalEvents / totalCurrentGovernors * 100m, 1) + : 0m; + } + + public static List GetGovernorsExcludingLeadership(TrustGovernance trustGovernance) + { + return trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .Concat(trustGovernance.HistoricMembers) + .Where(g => !g.HasRoleLeadership) + .ToList(); + } + + public static List GetCurrentGovernors(TrustGovernance trustGovernance) + { + return trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .ToList(); + } + + public static int CountEventsWithinDateRange(IEnumerable items, Func dateSelector, DateTime rangeStart, DateTime rangeEnd) + { + return items.Count(item => dateSelector(item) != null && + dateSelector(item) >= rangeStart && + dateSelector(item) <= rangeEnd); + } + } diff --git a/tests/DfE.FindInformationAcademiesTrusts.Data.UnitTests/DateTimeProviderTests.cs b/tests/DfE.FindInformationAcademiesTrusts.Data.UnitTests/DateTimeProviderTests.cs index df18832cb..77858b05c 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.Data.UnitTests/DateTimeProviderTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.Data.UnitTests/DateTimeProviderTests.cs @@ -17,4 +17,19 @@ public void Now_ShouldReturnCurrentDateTime() result.Should().BeOnOrAfter(beforeNow); result.Should().BeOnOrBefore(afterNow); } + + [Fact] + public void Today_ShouldReturnCurrentDateWithoutTime() + { + // Arrange + IDateTimeProvider dateTimeProvider = new DateTimeProvider(); + DateTime expectedDate = DateTime.Today; + + // Act + DateTime result = dateTimeProvider.Today; + + // Assert + result.Should().Be(expectedDate); + result.TimeOfDay.Should().Be(TimeSpan.Zero); // Ensure time component is zero, as this is for 'today' and should have no time element + } } \ No newline at end of file diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Pages/Trusts/GovernanceModelTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Pages/Trusts/GovernanceModelTests.cs index c4eb1dc09..185c6f63c 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Pages/Trusts/GovernanceModelTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Pages/Trusts/GovernanceModelTests.cs @@ -59,7 +59,7 @@ public class GovernanceModelTests ); private static readonly TrustGovernanceServiceModel DummyTrustGovernanceServiceModel = - new([Leader], [Member], [Trustee], [Historic]); + new([Leader], [Member], [Trustee], [Historic], 0); private readonly MockDataSourceService _mockDataSourceService = new(); private readonly Mock _mockTrustRepository = new(); @@ -75,7 +75,7 @@ public GovernanceModelTests() _sut = new GovernanceModel(_mockDataSourceService.Object, new MockLogger().Object, _mockTrustRepository.Object) - { Uid = TestUid }; + { Uid = TestUid }; } [Fact] diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/ExportServiceTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/ExportServiceTests.cs index 66e497621..0c84c6cc8 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/ExportServiceTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/ExportServiceTests.cs @@ -277,4 +277,150 @@ public void CalculatePercentageFull_ShouldReturnExpectedResult(int? numberOfPupi // Assert Assert.Equal(expected, result); } + [Fact] + public async Task ExportAcademiesToSpreadsheet_ShouldHandleNullTrustSummaryAsync() + { + // Arrange + var uid = "some-uid"; + _mockTrustRepository.Setup(x => x.GetTrustSummaryAsync(uid)) + .ReturnsAsync((TrustSummary?)null); + + // Act + var result = await _sut.ExportAcademiesToSpreadsheetAsync(uid); + using var workbook = new XLWorkbook(new MemoryStream(result)); + var worksheet = workbook.Worksheet("Academies"); + + // Assert + worksheet.Cell(1, 1).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(2, 1).Value.ToString().Should().Be(string.Empty); + } + + [Fact] + public async Task ExportAcademiesToSpreadsheet_ShouldHandleMissingOfstedDataAsync() + { + // Arrange + var trustSummary = new TrustSummaryServiceModel("1", "Sample Trust", "Multi-academy trust", 1); + var academyUrn = "123456"; + + _mockTrustRepository.Setup(x => x.GetTrustSummaryAsync(trustSummary.Uid)) + .ReturnsAsync(new TrustSummary("Sample Trust", "Multi-academy trust")); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustDetailsAsync(trustSummary.Uid)) + .ReturnsAsync(new AcademyDetails[] + { + new(academyUrn, "Academy 1", "Type A", "Local Authority 1", "Urban"), + }); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustOfstedAsync(trustSummary.Uid)) + .ReturnsAsync(Array.Empty()); + + // Act + var result = await _sut.ExportAcademiesToSpreadsheetAsync(trustSummary.Uid); + using var workbook = new XLWorkbook(new MemoryStream(result)); + var worksheet = workbook.Worksheet("Academies"); + + // Assert + worksheet.Cell(4, 7).Value.ToString().Should().Be("Not yet inspected"); + worksheet.Cell(4, 8).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(4, 9).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(4, 10).Value.ToString().Should().Be("Not yet inspected"); + worksheet.Cell(4, 11).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(4, 12).Value.ToString().Should().Be(string.Empty); + } + + [Fact] + public async Task ExportAcademiesToSpreadsheet_ShouldHandleMissingPupilNumbersDataAsync() + { + // Arrange + var trustSummary = new TrustSummaryServiceModel("1", "Sample Trust", "Multi-academy trust", 1); + var academyUrn = "123456"; + + _mockTrustRepository.Setup(x => x.GetTrustSummaryAsync(trustSummary.Uid)) + .ReturnsAsync(new TrustSummary("Sample Trust", "Multi-academy trust")); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustDetailsAsync(trustSummary.Uid)) + .ReturnsAsync(new AcademyDetails[] + { + new(academyUrn, "Academy 1", "Type A", "Local Authority 1", "Urban"), + }); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustPupilNumbersAsync(trustSummary.Uid)) + .ReturnsAsync(Array.Empty()); + + // Act + var result = await _sut.ExportAcademiesToSpreadsheetAsync(trustSummary.Uid); + using var workbook = new XLWorkbook(new MemoryStream(result)); + var worksheet = workbook.Worksheet("Academies"); + + // Assert + worksheet.Cell(4, 15).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(4, 16).Value.ToString().Should().Be(string.Empty); + worksheet.Cell(4, 17).Value.ToString().Should().Be(string.Empty); + } + + [Fact] + public async Task ExportAcademiesToSpreadsheet_ShouldHandleZeroPercentageFullAsync() + { + // Arrange + var trustSummary = new TrustSummaryServiceModel("1", "Sample Trust", "Multi-academy trust", 1); + var academyUrn = "123456"; + + _mockTrustRepository.Setup(x => x.GetTrustSummaryAsync(trustSummary.Uid)) + .ReturnsAsync(new TrustSummary("Sample Trust", "Multi-academy trust")); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustDetailsAsync(trustSummary.Uid)) + .ReturnsAsync(new AcademyDetails[] + { + new(academyUrn, "Academy 1", "Type A", "Local Authority 1", "Urban"), + }); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustPupilNumbersAsync(trustSummary.Uid)) + .ReturnsAsync(new AcademyPupilNumbers[] + { + new(academyUrn, "Academy 1", "Primary", new AgeRange(5,11), 0, 300) + }); + + // Act + var result = await _sut.ExportAcademiesToSpreadsheetAsync(trustSummary.Uid); + using var workbook = new XLWorkbook(new MemoryStream(result)); + var worksheet = workbook.Worksheet("Academies"); + + // Assert + worksheet.Cell(4, 17).Value.ToString().Should().Be(string.Empty); // % Full should be empty + } + + [Fact] + public async Task ExportAcademiesToSpreadsheet_ShouldHandleMissingFreeSchoolMealsDataAsync() + { + // Arrange + var trustSummary = new TrustSummaryServiceModel("1", "Sample Trust", "Multi-academy trust", 1); + var academyUrn = "123456"; + + _mockTrustRepository.Setup(x => x.GetTrustSummaryAsync(trustSummary.Uid)) + .ReturnsAsync(new TrustSummary("Sample Trust", "Multi-academy trust")); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustDetailsAsync(trustSummary.Uid)) + .ReturnsAsync(new AcademyDetails[] + { + new(academyUrn, "Academy 1", "Type A", "Local Authority 1", "Urban"), + }); + _mockAcademyRepository.Setup(m => m.GetAcademiesInTrustFreeSchoolMealsAsync(trustSummary.Uid)) + .ReturnsAsync(Array.Empty()); + + // Act + var result = await _sut.ExportAcademiesToSpreadsheetAsync(trustSummary.Uid); + using var workbook = new XLWorkbook(new MemoryStream(result)); + var worksheet = workbook.Worksheet("Academies"); + + // Assert + worksheet.Cell(4, 18).Value.ToString().Should().Be(string.Empty); + } + + [Fact] + public void IsOfstedRatingBeforeOrAfterJoining_ShouldReturnAfterJoining_WhenInspectionDateIsEqualToJoiningDate() + { + // Arrange + var ofstedRatingScore = OfstedRatingScore.Good; + var dateJoinedTrust = _mockDateTimeProvider.Object.Now; + DateTime? inspectionEndDate = dateJoinedTrust; + + // Act + var result = ExportService.IsOfstedRatingBeforeOrAfterJoining(ofstedRatingScore, dateJoinedTrust, inspectionEndDate); + + // Assert + result.Should().Be("After Joining"); + } } diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs new file mode 100644 index 000000000..8109e5fe9 --- /dev/null +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -0,0 +1,163 @@ +using DfE.FindInformationAcademiesTrusts.Data; +using DfE.FindInformationAcademiesTrusts.Data.FiatDb.Repositories; +using DfE.FindInformationAcademiesTrusts.Data.Repositories.Academy; +using DfE.FindInformationAcademiesTrusts.Data.Repositories.Trust; +using DfE.FindInformationAcademiesTrusts.Services.Trust; +using DfE.FindInformationAcademiesTrusts.UnitTests.Mocks; + +namespace DfE.FindInformationAcademiesTrusts.UnitTests.Services; + +public class TrustServiceGovernanceTurnoverTests +{ + private readonly TrustService _sut; + private readonly Mock _mockAcademyRepository = new(); + private readonly Mock _mockTrustRepository = new(); + private readonly Mock _mockContactRepository = new(); + private readonly Mock _mockDateTimeProvider = new(); + private readonly MockMemoryCache _mockMemoryCache = new(); + + public TrustServiceGovernanceTurnoverTests() + { + _sut = new TrustService(_mockAcademyRepository.Object, + _mockTrustRepository.Object, + _mockContactRepository.Object, + _mockMemoryCache.Object, + _mockDateTimeProvider.Object); + } + [Fact] + public void GetGovernanceTurnoverRate_Returns_Zero_When_No_CurrentGovernors() + { + // Arrange + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: [], + HistoricMembers: [] + ); + + _mockDateTimeProvider.Setup(d => d.Today).Returns(new DateTime(2023, 10, 1)); + + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); + + // Assert + result.Should().Be(0m); + } + + [Fact] + public void GetGovernanceTurnoverRate_Calculates_CorrectTurnover() + { + // Arrange + var startDate = new DateTime(2022, 1, 1); + var endDate = new DateTime(2023, 1, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(new DateTime(2023, 10, 1)); + + var governor = new Governor("1", "UID", "John Doe", "Member", "Appointing Body", startDate, endDate, null); + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [governor], + CurrentTrustees: [], + HistoricMembers: [governor] + ); + + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); + + // Assert + result.Should().BeGreaterThan(0m); // Check if it calculates a rate instead of zero + } + + [Fact] + public void GetGovernorsExcludingLeadership_Excludes_LeadershipRoles() + { + // Arrange + var leaderGovernor = new Governor("1", "UID", "John Doe", "Chair of Trustees", "Appointing Body", null, null, null); + var trusteeGovernor = new Governor("2", "UID2", "Jane Doe", "Trustee", "Appointing Body", null, null, null); + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [leaderGovernor], + CurrentMembers: [trusteeGovernor], + CurrentTrustees: [], + HistoricMembers: [] + ); + + // Act + var result = TrustService.GetGovernorsExcludingLeadership(trustGovernance); + + // Assert + result.Should().Contain(trusteeGovernor); + result.Should().NotContain(leaderGovernor); // Ensure leadership roles are excluded + } + + [Fact] + public void GetCurrentGovernors_Returns_AllCurrentMembersAndTrustees() + { + // Arrange + var member = new Governor("1", "UID1", "John Doe", "Member", "Appointing Body", null, null, null); + var trustee = new Governor("2", "UID2", "Jane Doe", "Trustee", "Appointing Body", null, null, null); + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [member], + CurrentTrustees: [trustee], + HistoricMembers: [] + ); + + // Act + var result = TrustService.GetCurrentGovernors(trustGovernance); + + // Assert + result.Should().Contain(member); + result.Should().Contain(trustee); + } + + [Theory] + [InlineData("2023-01-01", "2023-12-31", 2)] + [InlineData("2022-01-01", "2022-05-15", 1)] + [InlineData("2021-01-01", "2021-12-31", 0)] + public void CountEventsWithinDateRange_Calculates_CorrectCount( + string rangeStart, string rangeEnd, int expectedCount) + { + // Arrange + var startDate = DateTime.Parse(rangeStart); + var endDate = DateTime.Parse(rangeEnd); + var governors = new List + { + new Governor("1", "UID1", "John Doe", "Trustee", "Appointing Body", new DateTime(2023, 1, 1), null, null), + new Governor("2", "UID2", "Jane Doe", "Member", "Appointing Body", new DateTime(2023, 5, 15), null, null), + new Governor("3", "UID3", "Jake Doe", "Trustee", "Appointing Body", new DateTime(2022, 5, 15), null, null) + }; + + // Act + var result = TrustService.CountEventsWithinDateRange(governors, g => g.DateOfAppointment, startDate, endDate); + + // Assert + result.Should().Be(expectedCount); + } + + [Theory] + [InlineData(0, 0, 0.0)] + [InlineData(0, 10, 0.0)] + public void CalculateTurnoverRate_Returns_Zero_When_TotalCurrentGovernors_Is_Zero( + int totalCurrentGovernors, int totalEvents, decimal expectedRate) + { + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); + + // Assert + result.Should().Be(expectedRate); + } + + [Theory] + [InlineData(10, 5, 50.0)] + [InlineData(4, 3, 75.0)] + [InlineData(3, 1, 33.3)] + [InlineData(10, 0, 0.0)] + public void CalculateTurnoverRate_Calculates_CorrectRate_When_TotalCurrentGovernors_Is_Greater_Than_Zero( + int totalCurrentGovernors, int totalEvents, decimal expectedRate) + { + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); + + // Assert + result.Should().Be(expectedRate); + } +} diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceTests.cs index 59e456759..3477899e5 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceTests.cs @@ -24,12 +24,16 @@ public class TrustServiceTests private readonly Mock _mockAcademyRepository = new(); private readonly Mock _mockTrustRepository = new(); private readonly Mock _mockContactRepository = new(); + private readonly Mock _mockDateTimeProvider = new(); private readonly MockMemoryCache _mockMemoryCache = new(); public TrustServiceTests() { - _sut = new TrustService(_mockAcademyRepository.Object, _mockTrustRepository.Object, - _mockContactRepository.Object, _mockMemoryCache.Object); + _sut = new TrustService(_mockAcademyRepository.Object, + _mockTrustRepository.Object, + _mockContactRepository.Object, + _mockMemoryCache.Object, + _mockDateTimeProvider.Object); } [Fact] @@ -102,6 +106,8 @@ public async Task GetTrustGovernanceAsync_should_get_governanceResults_for_singl var startDate = DateTime.Today.AddYears(-3); var futureEndDate = DateTime.Today.AddYears(1); var historicEndDate = DateTime.Today.AddYears(-1); + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); var member = new Governor( "9999", "1234", @@ -148,9 +154,9 @@ public async Task GetTrustGovernanceAsync_should_get_governanceResults_for_singl var result = await _sut.GetTrustGovernanceAsync("1234"); result.HistoricMembers.Should().ContainSingle().Which.Should().BeEquivalentTo(historic); - result.Members.Should().ContainSingle().Which.Should().BeEquivalentTo(member); - result.Trustees.Should().ContainSingle().Which.Should().BeEquivalentTo(trustee); - result.TrustLeadership.Should().ContainSingle().Which.Should().BeEquivalentTo(leader); + result.CurrentMembers.Should().ContainSingle().Which.Should().BeEquivalentTo(member); + result.CurrentTrustees.Should().ContainSingle().Which.Should().BeEquivalentTo(trustee); + result.CurrentTrustLeadership.Should().ContainSingle().Which.Should().BeEquivalentTo(leader); } [Fact]