From 9745cb0aa6893879afdc230d3cccbc258ce9b3e7 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 10:58:57 +0000 Subject: [PATCH 01/10] Added Governance turnover --- CHANGELOG.md | 4 + .../DateTimeProvider.cs | 4 +- .../Repositories/Trust/TrustGovernance.cs | 6 +- .../Pages/Trusts/Governance.cshtml | 348 +++++++++--------- .../Trust/TrustGovernanceServiceModel.cs | 11 +- .../Services/Trust/TrustService.cs | 57 ++- .../DateTimeProviderTests.cs | 15 + .../Pages/Trusts/GovernanceModelTests.cs | 4 +- .../Services/ExportServiceTests.cs | 146 ++++++++ .../TrustServiceGovernanceTurnoverTests.cs | 284 ++++++++++++++ .../Services/TrustServiceTests.cs | 16 +- 11 files changed, 710 insertions(+), 185 deletions(-) create mode 100644 tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d07e16a7a..8a94766ea 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 + ## [Release-12][release-12] (production-2024-11-13.3974) ### Changed 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..9baed3fce 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 +

+

Number of trustees appointed or resigned as a proportion of the number of trustees on the board.

+
+
+
+
+
+
+
+ @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..a9465dafb 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 = CalculateTurnoverRate(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,46 @@ public async Task GetTrustOverviewAsync(string uid) return overviewModel; } + public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) + { + var today = dateTimeProvider.Today; + + // Past 12 Months + var past12MonthsStart = today.AddYears(-1); + + // Get current governors (Trustees and Members) + var currentGovernors = trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .ToList(); + + + // Get all governors for event calculations (including HistoricMembers), excluding specified roles + var allGovernorsExcludingLeadership = trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .Concat(trustGovernance.HistoricMembers) + .Where(g => !g.HasRoleLeadership) + .ToList(); + + // Total number of current governor positions + int totalCurrentGovernors = currentGovernors.Count; + + // Appointments in the past 12 months + int appointmentsInPast12Months = allGovernorsExcludingLeadership + .Count(g => g.DateOfAppointment != null && + g.DateOfAppointment >= past12MonthsStart && + g.DateOfAppointment <= today); + + // Resignations in the past 12 months + int resignationsInPast12Months = allGovernorsExcludingLeadership + .Count(g => g.DateOfTermEnd != null && + g.DateOfTermEnd >= past12MonthsStart && + g.DateOfTermEnd <= today); + + int totalEvents = appointmentsInPast12Months + resignationsInPast12Months; + + // Calculate turnover rate and round to 1 decimal point + return totalCurrentGovernors > 0 + ? Math.Round((decimal)totalEvents / totalCurrentGovernors * 100m, 1) + : 0m; + } } 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..73f725459 --- /dev/null +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -0,0 +1,284 @@ +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); + } + + private Governor CreateGovernor( + string role, + DateTime? dateOfAppointment, + DateTime? dateOfTermEnd) + { + return new Governor( + GID: Guid.NewGuid().ToString(), + UID: Guid.NewGuid().ToString(), + FullName: "Test Governor", + Role: role, + AppointingBody: "Test Body", + DateOfAppointment: dateOfAppointment, + DateOfTermEnd: dateOfTermEnd, + Email: "test@example.com"); + } + + [Fact] + public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() + { + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: [], + HistoricMembers: [] + ); + + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0m); + } + + [Fact] + public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddYears(-2), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0m); + } + + [Fact] + public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-6), null), + CreateGovernor("Trustee", today.AddMonths(-13), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(50.0m); + } + + [Fact] + public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var historicMembers = new List + { + CreateGovernor("Trustee", today.AddYears(-2), today.AddMonths(-6)) + }; + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddYears(-3), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: historicMembers.ToArray() + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-6), null), + CreateGovernor("Trustee", today.AddYears(-2), null) + }; + + var historicMembers = new List + { + CreateGovernor("Trustee", today.AddYears(-3), today.AddMonths(-4)) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: historicMembers.ToArray() + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_ExcludesLeadershipRoles() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var leadershipGovernor = CreateGovernor("Chair of Trustees", today.AddMonths(-6), null); + var trusteeGovernor = CreateGovernor("Trustee", today.AddMonths(-6), null); + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: new[] { leadershipGovernor }, + CurrentMembers: [], + CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(50.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() + { + var today = new DateTime(2023, 10, 1); + var past12MonthsStart = today.AddYears(-1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", past12MonthsStart, null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today, null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Excluded() + { + var today = new DateTime(2023, 10, 1); + var past12MonthsStart = today.AddYears(-1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", past12MonthsStart.AddDays(-1), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0.0m); + } + + [Fact] + public void CalculateTurnoverRate_RoundsToOneDecimalPlace() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-1), null), + CreateGovernor("Trustee", today.AddMonths(-2), null), + CreateGovernor("Trustee", today.AddMonths(-13), null), + CreateGovernor("Trustee", today.AddMonths(-14), null), + CreateGovernor("Trustee", today.AddMonths(-15), null), + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(40.0m); + } +} \ No newline at end of file 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] From f602ebdc476080216b70613db471e03ca2845190 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 13:34:01 +0000 Subject: [PATCH 02/10] Adjusted array instantiated in turnover tests --- .../TrustServiceGovernanceTurnoverTests.cs | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index 73f725459..e2229a1a2 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -45,10 +45,10 @@ private Governor CreateGovernor( public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() { var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], - CurrentTrustees: [], - HistoricMembers: [] + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), + CurrentTrustees: Array.Empty(), + HistoricMembers: Array.Empty() ); var today = new DateTime(2023, 10, 1); @@ -71,10 +71,10 @@ public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -95,10 +95,10 @@ public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -123,8 +123,8 @@ public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), HistoricMembers: historicMembers.ToArray() ); @@ -152,8 +152,8 @@ public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrect }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), HistoricMembers: historicMembers.ToArray() ); @@ -174,9 +174,9 @@ public void CalculateTurnoverRate_ExcludesLeadershipRoles() var trustGovernance = new TrustGovernance( CurrentTrustLeadership: new[] { leadershipGovernor }, - CurrentMembers: [], + CurrentMembers: Array.Empty(), CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -197,10 +197,10 @@ public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -220,10 +220,10 @@ public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -244,10 +244,10 @@ public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Exclude }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -271,10 +271,10 @@ public void CalculateTurnoverRate_RoundsToOneDecimalPlace() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); From 9790fb109d458135ad753f683875e96e44bc8408 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 14:37:27 +0000 Subject: [PATCH 03/10] Refactor governance turnover --- .../Services/Trust/TrustService.cs | 62 ++-- .../TrustServiceGovernanceTurnoverTests.cs | 305 ++++++------------ 2 files changed, 136 insertions(+), 231 deletions(-) diff --git a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs index a9465dafb..085a94551 100644 --- a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs +++ b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs @@ -58,7 +58,7 @@ public async Task GetTrustGovernanceAsync(string ui var trustGovernance = await trustRepository.GetTrustGovernanceAsync(uid, urn); - var governanceTurnover = CalculateTurnoverRate(trustGovernance); + var governanceTurnover = GetGovernanceTurnoverRate(trustGovernance); return new TrustGovernanceServiceModel( trustGovernance.CurrentTrustLeadership, @@ -136,7 +136,7 @@ public async Task GetTrustOverviewAsync(string uid) return overviewModel; } - public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) + public decimal GetGovernanceTurnoverRate(TrustGovernance trustGovernance) { var today = dateTimeProvider.Today; @@ -144,38 +144,64 @@ public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) var past12MonthsStart = today.AddYears(-1); // Get current governors (Trustees and Members) - var currentGovernors = trustGovernance.CurrentTrustees - .Concat(trustGovernance.CurrentMembers) - .ToList(); + List currentGovernors = GetCurrentGovernors(trustGovernance); // Get all governors for event calculations (including HistoricMembers), excluding specified roles - var allGovernorsExcludingLeadership = trustGovernance.CurrentTrustees - .Concat(trustGovernance.CurrentMembers) - .Concat(trustGovernance.HistoricMembers) - .Where(g => !g.HasRoleLeadership) - .ToList(); + List eligibleGovernorsForTurnoverCalculation = GetGovernorsExcludingLeadership(trustGovernance); // Total number of current governor positions int totalCurrentGovernors = currentGovernors.Count; // Appointments in the past 12 months - int appointmentsInPast12Months = allGovernorsExcludingLeadership - .Count(g => g.DateOfAppointment != null && - g.DateOfAppointment >= past12MonthsStart && - g.DateOfAppointment <= today); + int appointmentsInPast12Months = CountEventsWithinDateRange( + eligibleGovernorsForTurnoverCalculation, + g => g.DateOfAppointment, + past12MonthsStart, + today + ); // Resignations in the past 12 months - int resignationsInPast12Months = allGovernorsExcludingLeadership - .Count(g => g.DateOfTermEnd != null && - g.DateOfTermEnd >= past12MonthsStart && - g.DateOfTermEnd <= today); + 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.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index e2229a1a2..3c3a778d7 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -24,261 +24,140 @@ public TrustServiceGovernanceTurnoverTests() _mockMemoryCache.Object, _mockDateTimeProvider.Object); } - - private Governor CreateGovernor( - string role, - DateTime? dateOfAppointment, - DateTime? dateOfTermEnd) - { - return new Governor( - GID: Guid.NewGuid().ToString(), - UID: Guid.NewGuid().ToString(), - FullName: "Test Governor", - Role: role, - AppointingBody: "Test Body", - DateOfAppointment: dateOfAppointment, - DateOfTermEnd: dateOfTermEnd, - Email: "test@example.com"); - } - [Fact] - public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() + public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() { + // Arrange var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: Array.Empty(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: [], + HistoricMembers: [] ); - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + _mockDateTimeProvider.Setup(d => d.Today).Returns(new DateTime(2023, 10, 1)); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); + // Assert result.Should().Be(0m); } [Fact] - public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() + public void CalculateTurnoverRate_Calculates_CorrectTurnover() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddYears(-2), null) - }; + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [], + CurrentMembers: [governor], + CurrentTrustees: [], + HistoricMembers: [governor] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); - result.Should().Be(0m); + // Assert + result.Should().BeGreaterThan(0m); // Check if it calculates a rate instead of zero } [Fact] - public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() + public void GetAllGovernorsForEvents_Excludes_LeadershipRoles() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-6), null), - CreateGovernor("Trustee", today.AddMonths(-13), null) - }; - + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [leaderGovernor], + CurrentMembers: [trusteeGovernor], + CurrentTrustees: [], + HistoricMembers: [] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.GetGovernorsExcludingLeadership(trustGovernance); - result.Should().Be(50.0m); + // Assert + result.Should().Contain(trusteeGovernor); + result.Should().NotContain(leaderGovernor); // Ensure leadership roles are excluded } [Fact] - public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() + public void GetCurrentGovernors_Returns_AllCurrentMembersAndTrustees() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var historicMembers = new List - { - CreateGovernor("Trustee", today.AddYears(-2), today.AddMonths(-6)) - }; - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddYears(-3), null) - }; - + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: historicMembers.ToArray() + CurrentTrustLeadership: [], + CurrentMembers: [member], + CurrentTrustees: [trustee], + HistoricMembers: [] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.GetCurrentGovernors(trustGovernance); - result.Should().Be(100.0m); + // Assert + result.Should().Contain(member); + result.Should().Contain(trustee); } - [Fact] - public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrectRate() + [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 CountWithinDateRange_Calculates_CorrectCount( + string rangeStart, string rangeEnd, int expectedCount) { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-6), null), - CreateGovernor("Trustee", today.AddYears(-2), null) - }; - - var historicMembers = new List - { - CreateGovernor("Trustee", today.AddYears(-3), today.AddMonths(-4)) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: historicMembers.ToArray() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); + // 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); } - [Fact] - public void CalculateTurnoverRate_ExcludesLeadershipRoles() + [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) { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var leadershipGovernor = CreateGovernor("Chair of Trustees", today.AddMonths(-6), null); - var trusteeGovernor = CreateGovernor("Trustee", today.AddMonths(-6), null); - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: new[] { leadershipGovernor }, - CurrentMembers: Array.Empty(), - CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, - HistoricMembers: Array.Empty() - ); + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(50.0m); + // Assert + result.Should().Be(expectedRate); } - [Fact] - public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() + [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) { - var today = new DateTime(2023, 10, 1); - var past12MonthsStart = today.AddYears(-1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", past12MonthsStart, null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); - } - - [Fact] - public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() - { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today, null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); - } - - [Fact] - public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Excluded() - { - var today = new DateTime(2023, 10, 1); - var past12MonthsStart = today.AddYears(-1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", past12MonthsStart.AddDays(-1), null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(0.0m); - } - - [Fact] - public void CalculateTurnoverRate_RoundsToOneDecimalPlace() - { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-1), null), - CreateGovernor("Trustee", today.AddMonths(-2), null), - CreateGovernor("Trustee", today.AddMonths(-13), null), - CreateGovernor("Trustee", today.AddMonths(-14), null), - CreateGovernor("Trustee", today.AddMonths(-15), null), - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); - result.Should().Be(40.0m); + // Assert + result.Should().Be(expectedRate); } -} \ No newline at end of file +} From 92c52af41e1f7f1f904c6690e78e5aa602787d9a Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 14:50:33 +0000 Subject: [PATCH 04/10] Updated test names to reflect new method naming in turnover --- .../Services/TrustServiceGovernanceTurnoverTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index 3c3a778d7..8109e5fe9 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -25,7 +25,7 @@ public TrustServiceGovernanceTurnoverTests() _mockDateTimeProvider.Object); } [Fact] - public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() + public void GetGovernanceTurnoverRate_Returns_Zero_When_No_CurrentGovernors() { // Arrange var trustGovernance = new TrustGovernance( @@ -45,7 +45,7 @@ public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() } [Fact] - public void CalculateTurnoverRate_Calculates_CorrectTurnover() + public void GetGovernanceTurnoverRate_Calculates_CorrectTurnover() { // Arrange var startDate = new DateTime(2022, 1, 1); @@ -68,7 +68,7 @@ public void CalculateTurnoverRate_Calculates_CorrectTurnover() } [Fact] - public void GetAllGovernorsForEvents_Excludes_LeadershipRoles() + public void GetGovernorsExcludingLeadership_Excludes_LeadershipRoles() { // Arrange var leaderGovernor = new Governor("1", "UID", "John Doe", "Chair of Trustees", "Appointing Body", null, null, null); @@ -113,7 +113,7 @@ public void GetCurrentGovernors_Returns_AllCurrentMembersAndTrustees() [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 CountWithinDateRange_Calculates_CorrectCount( + public void CountEventsWithinDateRange_Calculates_CorrectCount( string rangeStart, string rangeEnd, int expectedCount) { // Arrange From bd508424e6b14f75c146651801ff4a0014ff7ec8 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 10:58:57 +0000 Subject: [PATCH 05/10] Added Governance turnover --- CHANGELOG.md | 4 + .../DateTimeProvider.cs | 4 +- .../Repositories/Trust/TrustGovernance.cs | 6 +- .../Pages/Trusts/Governance.cshtml | 348 +++++++++--------- .../Trust/TrustGovernanceServiceModel.cs | 11 +- .../Services/Trust/TrustService.cs | 57 ++- .../DateTimeProviderTests.cs | 15 + .../Pages/Trusts/GovernanceModelTests.cs | 4 +- .../Services/ExportServiceTests.cs | 146 ++++++++ .../TrustServiceGovernanceTurnoverTests.cs | 284 ++++++++++++++ .../Services/TrustServiceTests.cs | 16 +- 11 files changed, 710 insertions(+), 185 deletions(-) create mode 100644 tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d07e16a7a..8a94766ea 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 + ## [Release-12][release-12] (production-2024-11-13.3974) ### Changed 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..9baed3fce 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 +

+

Number of trustees appointed or resigned as a proportion of the number of trustees on the board.

+
+
+
+
+
+
+
+ @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..a9465dafb 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 = CalculateTurnoverRate(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,46 @@ public async Task GetTrustOverviewAsync(string uid) return overviewModel; } + public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) + { + var today = dateTimeProvider.Today; + + // Past 12 Months + var past12MonthsStart = today.AddYears(-1); + + // Get current governors (Trustees and Members) + var currentGovernors = trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .ToList(); + + + // Get all governors for event calculations (including HistoricMembers), excluding specified roles + var allGovernorsExcludingLeadership = trustGovernance.CurrentTrustees + .Concat(trustGovernance.CurrentMembers) + .Concat(trustGovernance.HistoricMembers) + .Where(g => !g.HasRoleLeadership) + .ToList(); + + // Total number of current governor positions + int totalCurrentGovernors = currentGovernors.Count; + + // Appointments in the past 12 months + int appointmentsInPast12Months = allGovernorsExcludingLeadership + .Count(g => g.DateOfAppointment != null && + g.DateOfAppointment >= past12MonthsStart && + g.DateOfAppointment <= today); + + // Resignations in the past 12 months + int resignationsInPast12Months = allGovernorsExcludingLeadership + .Count(g => g.DateOfTermEnd != null && + g.DateOfTermEnd >= past12MonthsStart && + g.DateOfTermEnd <= today); + + int totalEvents = appointmentsInPast12Months + resignationsInPast12Months; + + // Calculate turnover rate and round to 1 decimal point + return totalCurrentGovernors > 0 + ? Math.Round((decimal)totalEvents / totalCurrentGovernors * 100m, 1) + : 0m; + } } 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..73f725459 --- /dev/null +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -0,0 +1,284 @@ +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); + } + + private Governor CreateGovernor( + string role, + DateTime? dateOfAppointment, + DateTime? dateOfTermEnd) + { + return new Governor( + GID: Guid.NewGuid().ToString(), + UID: Guid.NewGuid().ToString(), + FullName: "Test Governor", + Role: role, + AppointingBody: "Test Body", + DateOfAppointment: dateOfAppointment, + DateOfTermEnd: dateOfTermEnd, + Email: "test@example.com"); + } + + [Fact] + public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() + { + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: [], + HistoricMembers: [] + ); + + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0m); + } + + [Fact] + public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddYears(-2), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0m); + } + + [Fact] + public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-6), null), + CreateGovernor("Trustee", today.AddMonths(-13), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(50.0m); + } + + [Fact] + public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var historicMembers = new List + { + CreateGovernor("Trustee", today.AddYears(-2), today.AddMonths(-6)) + }; + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddYears(-3), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: historicMembers.ToArray() + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrectRate() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-6), null), + CreateGovernor("Trustee", today.AddYears(-2), null) + }; + + var historicMembers = new List + { + CreateGovernor("Trustee", today.AddYears(-3), today.AddMonths(-4)) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: historicMembers.ToArray() + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_ExcludesLeadershipRoles() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var leadershipGovernor = CreateGovernor("Chair of Trustees", today.AddMonths(-6), null); + var trusteeGovernor = CreateGovernor("Trustee", today.AddMonths(-6), null); + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: new[] { leadershipGovernor }, + CurrentMembers: [], + CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(50.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() + { + var today = new DateTime(2023, 10, 1); + var past12MonthsStart = today.AddYears(-1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", past12MonthsStart, null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today, null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(100.0m); + } + + [Fact] + public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Excluded() + { + var today = new DateTime(2023, 10, 1); + var past12MonthsStart = today.AddYears(-1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", past12MonthsStart.AddDays(-1), null) + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(0.0m); + } + + [Fact] + public void CalculateTurnoverRate_RoundsToOneDecimalPlace() + { + var today = new DateTime(2023, 10, 1); + _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + + var currentTrustees = new List + { + CreateGovernor("Trustee", today.AddMonths(-1), null), + CreateGovernor("Trustee", today.AddMonths(-2), null), + CreateGovernor("Trustee", today.AddMonths(-13), null), + CreateGovernor("Trustee", today.AddMonths(-14), null), + CreateGovernor("Trustee", today.AddMonths(-15), null), + }; + + var trustGovernance = new TrustGovernance( + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: currentTrustees.ToArray(), + HistoricMembers: [] + ); + + var result = _sut.CalculateTurnoverRate(trustGovernance); + + result.Should().Be(40.0m); + } +} \ No newline at end of file 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] From 9c5c0ea13a060f5e094ab0d12600f4e49f07b5cd Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 13:34:01 +0000 Subject: [PATCH 06/10] Adjusted array instantiated in turnover tests --- .../TrustServiceGovernanceTurnoverTests.cs | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index 73f725459..e2229a1a2 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -45,10 +45,10 @@ private Governor CreateGovernor( public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() { var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], - CurrentTrustees: [], - HistoricMembers: [] + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), + CurrentTrustees: Array.Empty(), + HistoricMembers: Array.Empty() ); var today = new DateTime(2023, 10, 1); @@ -71,10 +71,10 @@ public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -95,10 +95,10 @@ public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -123,8 +123,8 @@ public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), HistoricMembers: historicMembers.ToArray() ); @@ -152,8 +152,8 @@ public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrect }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), HistoricMembers: historicMembers.ToArray() ); @@ -174,9 +174,9 @@ public void CalculateTurnoverRate_ExcludesLeadershipRoles() var trustGovernance = new TrustGovernance( CurrentTrustLeadership: new[] { leadershipGovernor }, - CurrentMembers: [], + CurrentMembers: Array.Empty(), CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -197,10 +197,10 @@ public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -220,10 +220,10 @@ public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -244,10 +244,10 @@ public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Exclude }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); @@ -271,10 +271,10 @@ public void CalculateTurnoverRate_RoundsToOneDecimalPlace() }; var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: [], - CurrentMembers: [], + CurrentTrustLeadership: Array.Empty(), + CurrentMembers: Array.Empty(), CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: [] + HistoricMembers: Array.Empty() ); var result = _sut.CalculateTurnoverRate(trustGovernance); From 14aabc144d8eb4a65e949646566240c51b2a022b Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 14:37:27 +0000 Subject: [PATCH 07/10] Refactor governance turnover --- .../Services/Trust/TrustService.cs | 62 ++-- .../TrustServiceGovernanceTurnoverTests.cs | 305 ++++++------------ 2 files changed, 136 insertions(+), 231 deletions(-) diff --git a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs index a9465dafb..085a94551 100644 --- a/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs +++ b/DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs @@ -58,7 +58,7 @@ public async Task GetTrustGovernanceAsync(string ui var trustGovernance = await trustRepository.GetTrustGovernanceAsync(uid, urn); - var governanceTurnover = CalculateTurnoverRate(trustGovernance); + var governanceTurnover = GetGovernanceTurnoverRate(trustGovernance); return new TrustGovernanceServiceModel( trustGovernance.CurrentTrustLeadership, @@ -136,7 +136,7 @@ public async Task GetTrustOverviewAsync(string uid) return overviewModel; } - public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) + public decimal GetGovernanceTurnoverRate(TrustGovernance trustGovernance) { var today = dateTimeProvider.Today; @@ -144,38 +144,64 @@ public decimal CalculateTurnoverRate(TrustGovernance trustGovernance) var past12MonthsStart = today.AddYears(-1); // Get current governors (Trustees and Members) - var currentGovernors = trustGovernance.CurrentTrustees - .Concat(trustGovernance.CurrentMembers) - .ToList(); + List currentGovernors = GetCurrentGovernors(trustGovernance); // Get all governors for event calculations (including HistoricMembers), excluding specified roles - var allGovernorsExcludingLeadership = trustGovernance.CurrentTrustees - .Concat(trustGovernance.CurrentMembers) - .Concat(trustGovernance.HistoricMembers) - .Where(g => !g.HasRoleLeadership) - .ToList(); + List eligibleGovernorsForTurnoverCalculation = GetGovernorsExcludingLeadership(trustGovernance); // Total number of current governor positions int totalCurrentGovernors = currentGovernors.Count; // Appointments in the past 12 months - int appointmentsInPast12Months = allGovernorsExcludingLeadership - .Count(g => g.DateOfAppointment != null && - g.DateOfAppointment >= past12MonthsStart && - g.DateOfAppointment <= today); + int appointmentsInPast12Months = CountEventsWithinDateRange( + eligibleGovernorsForTurnoverCalculation, + g => g.DateOfAppointment, + past12MonthsStart, + today + ); // Resignations in the past 12 months - int resignationsInPast12Months = allGovernorsExcludingLeadership - .Count(g => g.DateOfTermEnd != null && - g.DateOfTermEnd >= past12MonthsStart && - g.DateOfTermEnd <= today); + 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.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index e2229a1a2..3c3a778d7 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -24,261 +24,140 @@ public TrustServiceGovernanceTurnoverTests() _mockMemoryCache.Object, _mockDateTimeProvider.Object); } - - private Governor CreateGovernor( - string role, - DateTime? dateOfAppointment, - DateTime? dateOfTermEnd) - { - return new Governor( - GID: Guid.NewGuid().ToString(), - UID: Guid.NewGuid().ToString(), - FullName: "Test Governor", - Role: role, - AppointingBody: "Test Body", - DateOfAppointment: dateOfAppointment, - DateOfTermEnd: dateOfTermEnd, - Email: "test@example.com"); - } - [Fact] - public void CalculateTurnoverRate_WhenTotalCurrentGovernorsIsZero_ReturnsZero() + public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() { + // Arrange var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: Array.Empty(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [], + CurrentMembers: [], + CurrentTrustees: [], + HistoricMembers: [] ); - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); + _mockDateTimeProvider.Setup(d => d.Today).Returns(new DateTime(2023, 10, 1)); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); + // Assert result.Should().Be(0m); } [Fact] - public void CalculateTurnoverRate_NoAppointmentsOrResignations_ReturnsZero() + public void CalculateTurnoverRate_Calculates_CorrectTurnover() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddYears(-2), null) - }; + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [], + CurrentMembers: [governor], + CurrentTrustees: [], + HistoricMembers: [governor] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = _sut.GetGovernanceTurnoverRate(trustGovernance); - result.Should().Be(0m); + // Assert + result.Should().BeGreaterThan(0m); // Check if it calculates a rate instead of zero } [Fact] - public void CalculateTurnoverRate_WithAppointments_ReturnsCorrectRate() + public void GetAllGovernorsForEvents_Excludes_LeadershipRoles() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-6), null), - CreateGovernor("Trustee", today.AddMonths(-13), null) - }; - + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() + CurrentTrustLeadership: [leaderGovernor], + CurrentMembers: [trusteeGovernor], + CurrentTrustees: [], + HistoricMembers: [] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.GetGovernorsExcludingLeadership(trustGovernance); - result.Should().Be(50.0m); + // Assert + result.Should().Contain(trusteeGovernor); + result.Should().NotContain(leaderGovernor); // Ensure leadership roles are excluded } [Fact] - public void CalculateTurnoverRate_WithResignations_ReturnsCorrectRate() + public void GetCurrentGovernors_Returns_AllCurrentMembersAndTrustees() { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var historicMembers = new List - { - CreateGovernor("Trustee", today.AddYears(-2), today.AddMonths(-6)) - }; - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddYears(-3), null) - }; - + // 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: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: historicMembers.ToArray() + CurrentTrustLeadership: [], + CurrentMembers: [member], + CurrentTrustees: [trustee], + HistoricMembers: [] ); - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.GetCurrentGovernors(trustGovernance); - result.Should().Be(100.0m); + // Assert + result.Should().Contain(member); + result.Should().Contain(trustee); } - [Fact] - public void CalculateTurnoverRate_WithAppointmentsAndResignations_ReturnsCorrectRate() + [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 CountWithinDateRange_Calculates_CorrectCount( + string rangeStart, string rangeEnd, int expectedCount) { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-6), null), - CreateGovernor("Trustee", today.AddYears(-2), null) - }; - - var historicMembers = new List - { - CreateGovernor("Trustee", today.AddYears(-3), today.AddMonths(-4)) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: historicMembers.ToArray() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); + // 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); } - [Fact] - public void CalculateTurnoverRate_ExcludesLeadershipRoles() + [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) { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var leadershipGovernor = CreateGovernor("Chair of Trustees", today.AddMonths(-6), null); - var trusteeGovernor = CreateGovernor("Trustee", today.AddMonths(-6), null); - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: new[] { leadershipGovernor }, - CurrentMembers: Array.Empty(), - CurrentTrustees: new[] { leadershipGovernor, trusteeGovernor }, - HistoricMembers: Array.Empty() - ); + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(50.0m); + // Assert + result.Should().Be(expectedRate); } - [Fact] - public void CalculateTurnoverRate_AppointmentOnPast12MonthsStartDate_Included() + [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) { - var today = new DateTime(2023, 10, 1); - var past12MonthsStart = today.AddYears(-1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", past12MonthsStart, null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); - } - - [Fact] - public void CalculateTurnoverRate_AppointmentOnTodayDate_Included() - { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today, null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(100.0m); - } - - [Fact] - public void CalculateTurnoverRate_AppointmentBeforePast12MonthsStartDate_Excluded() - { - var today = new DateTime(2023, 10, 1); - var past12MonthsStart = today.AddYears(-1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", past12MonthsStart.AddDays(-1), null) - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); - - result.Should().Be(0.0m); - } - - [Fact] - public void CalculateTurnoverRate_RoundsToOneDecimalPlace() - { - var today = new DateTime(2023, 10, 1); - _mockDateTimeProvider.Setup(d => d.Today).Returns(today); - - var currentTrustees = new List - { - CreateGovernor("Trustee", today.AddMonths(-1), null), - CreateGovernor("Trustee", today.AddMonths(-2), null), - CreateGovernor("Trustee", today.AddMonths(-13), null), - CreateGovernor("Trustee", today.AddMonths(-14), null), - CreateGovernor("Trustee", today.AddMonths(-15), null), - }; - - var trustGovernance = new TrustGovernance( - CurrentTrustLeadership: Array.Empty(), - CurrentMembers: Array.Empty(), - CurrentTrustees: currentTrustees.ToArray(), - HistoricMembers: Array.Empty() - ); - - var result = _sut.CalculateTurnoverRate(trustGovernance); + // Act + var result = TrustService.CalculateTurnoverRate(totalCurrentGovernors, totalEvents); - result.Should().Be(40.0m); + // Assert + result.Should().Be(expectedRate); } -} \ No newline at end of file +} From ebedb6601818f7c850cfc9b6d5ab7c1c3fba8611 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 14:50:33 +0000 Subject: [PATCH 08/10] Updated test names to reflect new method naming in turnover --- .../Services/TrustServiceGovernanceTurnoverTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs index 3c3a778d7..8109e5fe9 100644 --- a/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs +++ b/tests/DfE.FindInformationAcademiesTrusts.UnitTests/Services/TrustServiceGovernanceTurnoverTests.cs @@ -25,7 +25,7 @@ public TrustServiceGovernanceTurnoverTests() _mockDateTimeProvider.Object); } [Fact] - public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() + public void GetGovernanceTurnoverRate_Returns_Zero_When_No_CurrentGovernors() { // Arrange var trustGovernance = new TrustGovernance( @@ -45,7 +45,7 @@ public void CalculateTurnoverRate_Returns_Zero_When_No_CurrentGovernors() } [Fact] - public void CalculateTurnoverRate_Calculates_CorrectTurnover() + public void GetGovernanceTurnoverRate_Calculates_CorrectTurnover() { // Arrange var startDate = new DateTime(2022, 1, 1); @@ -68,7 +68,7 @@ public void CalculateTurnoverRate_Calculates_CorrectTurnover() } [Fact] - public void GetAllGovernorsForEvents_Excludes_LeadershipRoles() + public void GetGovernorsExcludingLeadership_Excludes_LeadershipRoles() { // Arrange var leaderGovernor = new Governor("1", "UID", "John Doe", "Chair of Trustees", "Appointing Body", null, null, null); @@ -113,7 +113,7 @@ public void GetCurrentGovernors_Returns_AllCurrentMembersAndTrustees() [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 CountWithinDateRange_Calculates_CorrectCount( + public void CountEventsWithinDateRange_Calculates_CorrectCount( string rangeStart, string rangeEnd, int expectedCount) { // Arrange From b746d51d45e6509282475af27897f1d68b796d47 Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 15:33:31 +0000 Subject: [PATCH 09/10] Update workding for goverenance turnover caluclation --- .../Pages/Trusts/Governance.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml index 9baed3fce..06329cbaa 100644 --- a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml +++ b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml @@ -15,7 +15,7 @@

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

-

Number of trustees appointed or resigned as a proportion of the number of trustees on the board.

+

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.

From 486d2117e8c4270debbdfe89fdbb8ed1e0e8a5bf Mon Sep 17 00:00:00 2001 From: Dominic NEED Date: Thu, 14 Nov 2024 15:36:31 +0000 Subject: [PATCH 10/10] Redundant whitespace removed --- .../Pages/Trusts/Governance.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml index 06329cbaa..de45cf6a8 100644 --- a/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml +++ b/DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml @@ -15,7 +15,7 @@

@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.

+

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.