Skip to content

Commit

Permalink
Merge pull request #614 from DFE-Digital/turnover-rate
Browse files Browse the repository at this point in the history
Turnover rate
  • Loading branch information
dneed-nimble authored Nov 14, 2024
2 parents 33ab9c4 + c348ec8 commit 7943488
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 185 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased][unreleased]

### Added

- Added Governance turnover to governance page

### Changed

- Updated the node version in the github runners and docker image
Expand Down
4 changes: 3 additions & 1 deletion DfE.FindInformationAcademiesTrusts.Data/DateTimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
}
Original file line number Diff line number Diff line change
@@ -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);
348 changes: 183 additions & 165 deletions DfE.FindInformationAcademiesTrusts/Pages/Trusts/Governance.cshtml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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);
83 changes: 78 additions & 5 deletions DfE.FindInformationAcademiesTrusts/Services/Trust/TrustService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,7 +22,8 @@ public class TrustService(
IAcademyRepository academyRepository,
ITrustRepository trustRepository,
IContactRepository contactRepository,
IMemoryCache memoryCache)
IMemoryCache memoryCache,
IDateTimeProvider dateTimeProvider)
: ITrustService
{
public async Task<TrustSummaryServiceModel?> GetTrustSummaryAsync(string uid)
Expand Down Expand Up @@ -56,11 +58,14 @@ public async Task<TrustGovernanceServiceModel> GetTrustGovernanceAsync(string ui

var trustGovernance = await trustRepository.GetTrustGovernanceAsync(uid, urn);

var governanceTurnover = GetGovernanceTurnoverRate(trustGovernance);

return new TrustGovernanceServiceModel(
trustGovernance.TrustLeadership,
trustGovernance.Members,
trustGovernance.Trustees,
trustGovernance.HistoricMembers);
trustGovernance.CurrentTrustLeadership,
trustGovernance.CurrentMembers,
trustGovernance.CurrentTrustees,
trustGovernance.HistoricMembers,
governanceTurnover);
}

public async Task<TrustContactsServiceModel> GetTrustContactsAsync(string uid)
Expand Down Expand Up @@ -131,4 +136,72 @@ public async Task<TrustOverviewServiceModel> GetTrustOverviewAsync(string uid)

return overviewModel;
}
public decimal GetGovernanceTurnoverRate(TrustGovernance trustGovernance)
{
var today = dateTimeProvider.Today;

// Past 12 Months
var past12MonthsStart = today.AddYears(-1);

// Get current governors (Trustees and Members)
List<Governor> currentGovernors = GetCurrentGovernors(trustGovernance);


// Get all governors for event calculations (including HistoricMembers), excluding specified roles
List<Governor> eligibleGovernorsForTurnoverCalculation = GetGovernorsExcludingLeadership(trustGovernance);

// Total number of current governor positions
int totalCurrentGovernors = currentGovernors.Count;

// Appointments in the past 12 months
int appointmentsInPast12Months = CountEventsWithinDateRange(
eligibleGovernorsForTurnoverCalculation,
g => g.DateOfAppointment,
past12MonthsStart,
today
);

// Resignations in the past 12 months
int resignationsInPast12Months = CountEventsWithinDateRange(
eligibleGovernorsForTurnoverCalculation,
g => g.DateOfTermEnd,
past12MonthsStart,
today
);

int totalEvents = appointmentsInPast12Months + resignationsInPast12Months;
return CalculateTurnoverRate(totalCurrentGovernors, totalEvents);
}

public static decimal CalculateTurnoverRate(int totalCurrentGovernors, int totalEvents)
{
// Calculate turnover rate and round to 1 decimal point
return totalCurrentGovernors > 0
? Math.Round((decimal)totalEvents / totalCurrentGovernors * 100m, 1)
: 0m;
}

public static List<Governor> GetGovernorsExcludingLeadership(TrustGovernance trustGovernance)
{
return trustGovernance.CurrentTrustees
.Concat(trustGovernance.CurrentMembers)
.Concat(trustGovernance.HistoricMembers)
.Where(g => !g.HasRoleLeadership)
.ToList();
}

public static List<Governor> GetCurrentGovernors(TrustGovernance trustGovernance)
{
return trustGovernance.CurrentTrustees
.Concat(trustGovernance.CurrentMembers)
.ToList();
}

public static int CountEventsWithinDateRange<T>(IEnumerable<T> items, Func<T, DateTime?> dateSelector, DateTime rangeStart, DateTime rangeEnd)
{
return items.Count(item => dateSelector(item) != null &&
dateSelector(item) >= rangeStart &&
dateSelector(item) <= rangeEnd);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITrustService> _mockTrustRepository = new();
Expand All @@ -75,7 +75,7 @@ public GovernanceModelTests()

_sut = new GovernanceModel(_mockDataSourceService.Object,
new MockLogger<GovernanceModel>().Object, _mockTrustRepository.Object)
{ Uid = TestUid };
{ Uid = TestUid };
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AcademyOfsted>());

// 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<AcademyPupilNumbers>());

// 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<AcademyFreeSchoolMeals>());

// 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");
}
}
Loading

0 comments on commit 7943488

Please sign in to comment.