From 17e94f8dcedd872265d4a5caa8ce2682a0a68f1e Mon Sep 17 00:00:00 2001 From: Marc Woolfson Date: Fri, 14 Mar 2025 07:52:23 +0000 Subject: [PATCH 1/3] feat: Wire up `HighNeedsService.Get()` to underlying database views as override to stubbed version Using Dapper multi mapping --- .../Platform.Sql/DatabaseConnection.cs | 99 +++++++----- .../QueryBuilders/LocalAuthorityQueries.cs | 26 ++- .../QueryBuilders/PlatformQuery.cs | 9 ++ .../Builders/PlatformQueryTests.cs | 16 ++ .../Services/LocalAuthoritiesService.cs | 1 - .../Features/HighNeeds/HighNeedsFeature.cs | 2 +- .../Features/HighNeeds/Models/HighNeeds.cs | 14 +- .../HighNeeds/Models/LocalAuthority.cs | 10 +- .../HighNeeds/Services/HighNeedsService.cs | 152 ++++++++++++++++++ .../Services/HighNeedsStubService.cs | 4 +- 10 files changed, 279 insertions(+), 54 deletions(-) diff --git a/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs b/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs index a7220ef7b..7b0ef9106 100644 --- a/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs +++ b/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs @@ -53,6 +53,27 @@ public Task> QueryAsync(string sql, object? param = null, Canc public Task> QueryAsync(PlatformQuery query, CancellationToken cancellationToken = default) => connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken)); + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default) => + connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn)); + + public Task> QueryAsync(PlatformQuery query, Type[] types, Func map, string[] splitOn) => + connection.QueryAsync(query.QueryTemplate.RawSql, types, map, query.QueryTemplate.Parameters, splitOn: string.Join(", ", splitOn)); + public Task QueryFirstOrDefaultAsync(string sql, object? param = null, CancellationToken cancellationToken = default) => connection.QueryFirstOrDefaultAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); @@ -73,58 +94,54 @@ public Task InsertAsync(T entityToInsert, IDbTransaction? transaction = public interface IDatabaseConnection : IDbConnection { - /// - /// Execute a query asynchronously using Task. - /// - /// The type of results to return. - /// The SQL to execute for the query. - /// The parameters to pass, if any. - /// The cancellation token for this command. - /// - /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from - /// the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// + /// Task> QueryAsync(string sql, object? param = null, CancellationToken cancellationToken = default); + /// Task> QueryAsync(PlatformQuery query, CancellationToken cancellationToken = default); - /// - /// Execute a single-row query asynchronously using Task. - /// - /// The type of result to return. - /// The SQL to execute for the query. - /// The parameters to pass, if any. - /// The cancellation token for this command. + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + Task> QueryAsync(PlatformQuery query, Func map, string[] splitOn, CancellationToken cancellationToken = default); + + /// + /// ℹ No support (see https://github.com/DapperLib/Dapper/issues/2125). + Task> QueryAsync(PlatformQuery query, Type[] types, Func map, string[] splitOn); + + /// Task QueryFirstOrDefaultAsync(string sql, object? param = null, CancellationToken cancellationToken = default); + /// Task QueryFirstOrDefaultAsync(PlatformQuery query, CancellationToken cancellationToken = default); - /// - /// Execute a command asynchronously using Task. - /// - /// The SQL to execute for this query. - /// The parameters to use for this query. - /// The transaction to use for this query. - /// The cancellation token for this command. - /// The number of rows affected. + /// Task ExecuteAsync(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default); - /// - /// Execute a single-row query asynchronously using Task. - /// - /// The type of result to return. - /// The SQL to execute for the query. - /// The parameters to pass, if any. - /// The cancellation token for this command. + /// Task QueryFirstAsync(string sql, object? param = null, CancellationToken cancellationToken = default); - /// - /// Inserts an entity into table "Ts" asynchronously using Task and returns identity id. - /// - /// The type being inserted. - /// Entity to insert - /// The transaction to run under, null (the default) if none - /// Identity of inserted entity + /// Task InsertAsync(T entityToInsert, IDbTransaction? transaction = null) where T : class; } \ No newline at end of file diff --git a/platform/src/abstractions/Platform.Sql/QueryBuilders/LocalAuthorityQueries.cs b/platform/src/abstractions/Platform.Sql/QueryBuilders/LocalAuthorityQueries.cs index 5b03369e8..c92a0286c 100644 --- a/platform/src/abstractions/Platform.Sql/QueryBuilders/LocalAuthorityQueries.cs +++ b/platform/src/abstractions/Platform.Sql/QueryBuilders/LocalAuthorityQueries.cs @@ -1,4 +1,6 @@ -namespace Platform.Sql.QueryBuilders; +using Platform.Domain; + +namespace Platform.Sql.QueryBuilders; public class LocalAuthorityQuery() : PlatformQuery(Sql) { @@ -8,4 +10,26 @@ public class LocalAuthorityQuery() : PlatformQuery(Sql) public class LocalAuthorityStatisticalNeighbourQuery() : PlatformQuery(Sql) { private const string Sql = "SELECT * FROM VW_LocalAuthorityStatisticalNeighbours /**where**/"; +} + +public class LocalAuthorityCurrentFinancialQuery : PlatformQuery +{ + public LocalAuthorityCurrentFinancialQuery(string dimension, string[] fields) : base(GetSql(dimension, fields)) + { + foreach (var field in fields) + { + Select(field); + } + } + + private static string GetSql(string dimension, string[] fields) + { + var select = fields.Length == 0 ? "*" : "/**select**/"; + return dimension switch + { + Dimensions.HighNeeds.Actuals => $"SELECT {select} FROM VW_LocalAuthorityFinancialDefaultCurrentActual /**where**/", + Dimensions.HighNeeds.PerHead => $"SELECT {select} FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation /**where**/", + _ => throw new ArgumentOutOfRangeException(nameof(dimension), "Unknown dimension") + }; + } } \ No newline at end of file diff --git a/platform/src/abstractions/Platform.Sql/QueryBuilders/PlatformQuery.cs b/platform/src/abstractions/Platform.Sql/QueryBuilders/PlatformQuery.cs index 35dce0e5d..1204981ff 100644 --- a/platform/src/abstractions/Platform.Sql/QueryBuilders/PlatformQuery.cs +++ b/platform/src/abstractions/Platform.Sql/QueryBuilders/PlatformQuery.cs @@ -83,6 +83,15 @@ public PlatformQuery WhereLaCodeEqual(string laCode) return this; } + public PlatformQuery WhereLaCodesIn(string[] laCodes) + { + const string sql = "LaCode IN @LaCodes"; + var parameters = new { LaCodes = laCodes }; + + Where(sql, parameters); + return this; + } + public PlatformQuery WhereCodeEqual(string code) { const string sql = "Code = @Code"; diff --git a/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/PlatformQueryTests.cs b/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/PlatformQueryTests.cs index ae937394c..5a18f4d80 100644 --- a/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/PlatformQueryTests.cs +++ b/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/PlatformQueryTests.cs @@ -137,6 +137,22 @@ public void ShouldAddLaCodeEqualParameter() Assert.Equal(expectedSql, builder.QueryTemplate.RawSql); } + [Fact] + public void ShouldAddLaCodeInParameter() + { + const string expectedParam = "LaCodes"; + var expectedValue = new[] { "12345", "12346" }; + var expectedSql = BuildExpectedQuery("WHERE LaCode IN @LaCodes"); + + var builder = new MockPlatformQuery().WhereLaCodesIn(expectedValue); + var parameters = builder.QueryTemplate.Parameters.GetTemplateParameters(expectedParam); + + Assert.Single(parameters); + Assert.Contains(expectedParam, parameters.Keys); + Assert.Equal(expectedValue, parameters[expectedParam]); + Assert.Equal(expectedSql, builder.QueryTemplate.RawSql); + } + [Fact] public void ShouldAddOverallPhaseEqualParameter() { diff --git a/platform/src/apis/Platform.Api.Establishment/Features/LocalAuthorities/Services/LocalAuthoritiesService.cs b/platform/src/apis/Platform.Api.Establishment/Features/LocalAuthorities/Services/LocalAuthoritiesService.cs index 59d59188c..a61f0db0b 100644 --- a/platform/src/apis/Platform.Api.Establishment/Features/LocalAuthorities/Services/LocalAuthoritiesService.cs +++ b/platform/src/apis/Platform.Api.Establishment/Features/LocalAuthorities/Services/LocalAuthoritiesService.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Platform.Api.Establishment.Features.LocalAuthorities.Models; using Platform.Api.Establishment.Features.LocalAuthorities.Requests; using Platform.Domain; diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/HighNeedsFeature.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/HighNeedsFeature.cs index 511eb1331..ffb073ed8 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/HighNeedsFeature.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/HighNeedsFeature.cs @@ -13,7 +13,7 @@ public static class HighNeedsFeature public static IServiceCollection AddHighNeedsFeature(this IServiceCollection serviceCollection) { serviceCollection - .AddSingleton(); + .AddSingleton(); serviceCollection .AddTransient, HighNeedsParametersValidator>() diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/HighNeeds.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/HighNeeds.cs index c5d587801..13e563c27 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/HighNeeds.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/HighNeeds.cs @@ -2,11 +2,15 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; -public record HighNeeds +public record HighNeeds : HighNeedsBase +{ + public HighNeedsAmount? HighNeedsAmount { get; set; } + public TopFunding? Maintained { get; set; } + public TopFunding? NonMaintained { get; set; } + public PlaceFunding? PlaceFunding { get; set; } +} + +public record HighNeedsBase { public decimal? Total { get; set; } - public HighNeedsAmount HighNeedsAmount { get; set; } = new(); - public TopFunding Maintained { get; set; } = new(); - public TopFunding NonMaintained { get; set; } = new(); - public PlaceFunding PlaceFunding { get; set; } = new(); } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/LocalAuthority.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/LocalAuthority.cs index 226363583..da548b1e7 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/LocalAuthority.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Models/LocalAuthority.cs @@ -2,10 +2,14 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; -public record LocalAuthority +public record LocalAuthority : LocalAuthorityBase { - public string? Code { get; set; } - public string? Name { get; set; } public T? Outturn { get; set; } public T? Budget { get; set; } +} + +public record LocalAuthorityBase +{ + public string? Code { get; set; } + public string? Name { get; set; } } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs index 1b771bc14..031b04864 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs @@ -1,6 +1,10 @@ +using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; +using Platform.Sql; +using Platform.Sql.QueryBuilders; namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Services; @@ -8,4 +12,152 @@ public interface IHighNeedsService { Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default); Task?> GetHistory(string[] codes, CancellationToken cancellationToken = default); +} + +public class HighNeedsService(IDatabaseFactory dbFactory) : HighNeedsStubService +{ + public override async Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default) + { + string[] fields = + [ + // LocalAuthorityBase + "LaCode AS Code", + "Name", + // HighNeedsBase + "OutturnTotalHighNeeds AS Total", + // HighNeedsAmount + "OutturnTotalPlaceFunding AS TotalPlaceFunding", + "OutturnTotalTopUpFundingMaintained AS TopUpFundingMaintained", + "OutturnTotalTopUpFundingNonMaintained AS TopUpFundingNonMaintained", + "OutturnTotalSenServices AS SenServices", + "OutturnTotalAlternativeProvisionServices AS AlternativeProvisionServices", + "OutturnTotalHospitalServices AS HospitalServices", + "OutturnTotalOtherHealthServices AS OtherHealthServices", + // TopFunding + "OutturnTopFundingMaintainedEarlyYears AS EarlyYears", + "OutturnTopFundingMaintainedPrimary AS [Primary]", + "OutturnTopFundingMaintainedSecondary AS Secondary", + "OutturnTopFundingMaintainedSpecial AS Special", + "OutturnTopFundingMaintainedAlternativeProvision AS AlternativeProvision", + "OutturnTopFundingMaintainedPostSchool AS PostSchool", + "OutturnTopFundingMaintainedIncome AS Income", + // TopFunding + "OutturnTopFundingNonMaintainedEarlyYears AS EarlyYears", + "OutturnTopFundingNonMaintainedPrimary AS [Primary]", + "OutturnTopFundingNonMaintainedSecondary AS Secondary", + "OutturnTopFundingNonMaintainedSpecial AS Special", + "OutturnTopFundingNonMaintainedAlternativeProvision AS AlternativeProvision", + "OutturnTopFundingNonMaintainedPostSchool AS PostSchool", + "OutturnTopFundingNonMaintainedIncome AS Income", + // PlaceFunding + "OutturnPlaceFundingPrimary AS [Primary]", + "OutturnPlaceFundingSecondary AS Secondary", + "OutturnPlaceFundingSpecial AS Special", + "OutturnPlaceFundingAlternativeProvision AS AlternativeProvision", + // HighNeedsBase + "BudgetTotalHighNeeds AS Total", + // HighNeedsAmount + "BudgetTotalPlaceFunding AS TotalPlaceFunding", + "BudgetTotalTopUpFundingMaintained AS TopUpFundingMaintained", + "BudgetTotalTopUpFundingNonMaintained AS TopUpFundingNonMaintained", + "BudgetTotalSenServices AS SenServices", + "BudgetTotalAlternativeProvisionServices AS AlternativeProvisionServices", + "BudgetTotalHospitalServices AS HospitalServices", + "BudgetTotalOtherHealthServices AS OtherHealthServices", + // TopFunding + "BudgetTopFundingMaintainedEarlyYears AS EarlyYears", + "BudgetTopFundingMaintainedPrimary AS [Primary]", + "BudgetTopFundingMaintainedSecondary AS Secondary", + "BudgetTopFundingMaintainedSpecial AS Special", + "BudgetTopFundingMaintainedAlternativeProvision AS AlternativeProvision", + "BudgetTopFundingMaintainedPostSchool AS PostSchool", + "BudgetTopFundingMaintainedIncome AS Income", + // TopFunding + "BudgetTopFundingNonMaintainedEarlyYears AS EarlyYears", + "BudgetTopFundingNonMaintainedPrimary AS [Primary]", + "BudgetTopFundingNonMaintainedSecondary AS Secondary", + "BudgetTopFundingNonMaintainedSpecial AS Special", + "BudgetTopFundingNonMaintainedAlternativeProvision AS AlternativeProvision", + "BudgetTopFundingNonMaintainedPostSchool AS PostSchool", + "BudgetTopFundingNonMaintainedIncome AS Income", + // PlaceFunding + "BudgetPlaceFundingPrimary AS [Primary]", + "BudgetPlaceFundingSecondary AS Secondary", + "BudgetPlaceFundingSpecial AS Special", + "BudgetPlaceFundingAlternativeProvision AS AlternativeProvision" + ]; + + Type[] types = + [ + typeof(LocalAuthorityBase), + typeof(HighNeedsBase), + typeof(HighNeedsAmount), + typeof(TopFunding), + typeof(TopFunding), + typeof(PlaceFunding), + typeof(HighNeedsBase), + typeof(HighNeedsAmount), + typeof(TopFunding), + typeof(TopFunding), + typeof(PlaceFunding) + ]; + + string[] splitOn = + [ + nameof(HighNeedsBase.Total), + nameof(HighNeedsAmount.TotalPlaceFunding), + nameof(TopFunding.EarlyYears), + nameof(TopFunding.EarlyYears), + nameof(PlaceFunding.Primary), + nameof(HighNeedsBase.Total), + nameof(HighNeedsAmount.TotalPlaceFunding), + nameof(TopFunding.EarlyYears), + nameof(TopFunding.EarlyYears), + nameof(PlaceFunding.Primary) + ]; + + using var conn = await dbFactory.GetConnection(); + var laBuilder = new LocalAuthorityCurrentFinancialQuery(dimension, fields) + .WhereLaCodesIn(codes); + + var results = await conn.QueryAsync(laBuilder, types, Map, splitOn); + return results.ToArray(); + + LocalAuthority Map(object[] objects) + { + var localAuthority = objects[0] as LocalAuthorityBase; + var outturn = objects[1] as HighNeedsBase; + var outturnHighNeedsAmount = objects[2] as HighNeedsAmount; + var outturnTopFundingMaintained = objects[3] as TopFunding; + var outturnTopFundingNonMaintained = objects[4] as TopFunding; + var outturnPlaceFunding = objects[5] as PlaceFunding; + var budget = objects[6] as HighNeedsBase; + var budgetHighNeedsAmount = objects[7] as HighNeedsAmount; + var budgeTopFundingMaintained = objects[8] as TopFunding; + var budgetTopFundingNonMaintained = objects[9] as TopFunding; + var budgetPlaceFunding = objects[10] as PlaceFunding; + + return new LocalAuthority + { + Code = localAuthority?.Code, + Name = localAuthority?.Name, + Outturn = new Models.HighNeeds + { + Total = outturn?.Total, + HighNeedsAmount = outturnHighNeedsAmount, + Maintained = outturnTopFundingMaintained, + NonMaintained = outturnTopFundingNonMaintained, + PlaceFunding = outturnPlaceFunding + }, + Budget = new Models.HighNeeds + { + Total = budget?.Total, + HighNeedsAmount = budgetHighNeedsAmount, + Maintained = budgeTopFundingMaintained, + NonMaintained = budgetTopFundingNonMaintained, + PlaceFunding = budgetPlaceFunding + } + }; + } + } } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs index 6eecfd9b9..02a6bc12a 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs @@ -12,12 +12,12 @@ namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Services; [SuppressMessage("Performance", "CA1822:Mark members as static")] public class HighNeedsStubService : IHighNeedsService { - public Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default) + public virtual Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default) { return Task.FromResult(codes.Select(c => GetLocalAuthority(c, dimension)).ToArray()); } - public Task?> GetHistory(string[] codes, CancellationToken cancellationToken = default) + public virtual Task?> GetHistory(string[] codes, CancellationToken cancellationToken = default) { var code = codes.First(); const int startYear = 2021; From 76c01c28aaa147ada2082aaf1638ceabe7f13ceb Mon Sep 17 00:00:00 2001 From: Marc Woolfson Date: Fri, 14 Mar 2025 10:53:58 +0000 Subject: [PATCH 2/3] feat: Unit tests for `HighNeedsService` so far, plus coverage for `LocalAuthorityCurrentFinancialQuery` --- .../Views/018-LocalAuthorityFinancial.sql | 2 +- .../Platform.Sql/DatabaseConnection.cs | 2 +- .../Builders/LocalAuthorityQueryTests.cs | 27 +++ .../HighNeeds/Services/HighNeedsService.cs | 166 +++++++++--------- .../Services/HighNeedsStubService.cs | 25 +-- .../Program.cs | 2 + .../WhenHighNeedsServiceQueriesAsync.cs | 111 ++++++++++++ 7 files changed, 228 insertions(+), 107 deletions(-) create mode 100644 platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs diff --git a/core-infrastructure/src/db/Core.Database/Views/018-LocalAuthorityFinancial.sql b/core-infrastructure/src/db/Core.Database/Views/018-LocalAuthorityFinancial.sql index 11df6bd7c..c4e596a9d 100644 --- a/core-infrastructure/src/db/Core.Database/Views/018-LocalAuthorityFinancial.sql +++ b/core-infrastructure/src/db/Core.Database/Views/018-LocalAuthorityFinancial.sql @@ -96,7 +96,7 @@ AS IIF(Population2To18 > 0.0, OutturnPlaceFundingPrimary / Population2To18, NULL) AS OutturnPlaceFundingPrimary, IIF(Population2To18 > 0.0, OutturnPlaceFundingSecondary / Population2To18, NULL) AS OutturnPlaceFundingSecondary, IIF(Population2To18 > 0.0, OutturnPlaceFundingSpecial / Population2To18, NULL) AS OutturnPlaceFundingSpecial, - IIF(Population2To18 > 0.0, OutturnPlaceFundingAlternativeProvision / Population2To18, NULL) AS TotalExOutturnPlaceFundingAlternativeProvisionpenditure, + IIF(Population2To18 > 0.0, OutturnPlaceFundingAlternativeProvision / Population2To18, NULL) AS OutturnPlaceFundingAlternativeProvision, IIF(Population2To18 > 0.0, BudgetTotalHighNeeds / Population2To18, NULL) AS BudgetTotalHighNeeds, IIF(Population2To18 > 0.0, BudgetTotalPlaceFunding / Population2To18, NULL) AS BudgetTotalPlaceFunding, IIF(Population2To18 > 0.0, BudgetTotalTopUpFundingMaintained / Population2To18, NULL) AS BudgetTotalTopUpFundingMaintained, diff --git a/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs b/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs index 7b0ef9106..f02075b09 100644 --- a/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs +++ b/platform/src/abstractions/Platform.Sql/DatabaseConnection.cs @@ -126,7 +126,7 @@ public interface IDatabaseConnection : IDbConnection /// - /// ℹ No support (see https://github.com/DapperLib/Dapper/issues/2125). + /// No support (see https://github.com/DapperLib/Dapper/issues/2125). Task> QueryAsync(PlatformQuery query, Type[] types, Func map, string[] splitOn); /// diff --git a/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/LocalAuthorityQueryTests.cs b/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/LocalAuthorityQueryTests.cs index 2eaa25669..b43f372a6 100644 --- a/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/LocalAuthorityQueryTests.cs +++ b/platform/src/abstractions/tests/Platform.Sql.Tests/Builders/LocalAuthorityQueryTests.cs @@ -13,4 +13,31 @@ public void ShouldReturnSql() } private static LocalAuthorityQuery Create() => new(); +} + +public class LocalAuthorityCurrentFinancialQueryTests +{ + [Theory] + [MemberData(nameof(Data))] + public void ShouldReturnSql(string dimension, string[] fields, string expected) + { + var builder = Create(dimension, fields); + Assert.Equal(expected, builder.QueryTemplate.RawSql); + } + + [Fact] + public void ShouldThrowArgumentOutOfRangeException() + { + Assert.Throws(() => Create("dimension", [])); + } + + public static TheoryData Data => new() + { + { "Actuals", [], "SELECT * FROM VW_LocalAuthorityFinancialDefaultCurrentActual " }, + { "Actuals", ["field1", "field2"], "SELECT field1 , field2\n FROM VW_LocalAuthorityFinancialDefaultCurrentActual " }, + { "PerHead", [], "SELECT * FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation " }, + { "PerHead", ["field1", "field2"], "SELECT field1 , field2\n FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation " }, + }; + + private static LocalAuthorityCurrentFinancialQuery Create(string dimension, string[] fields) => new(dimension, fields); } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs index 031b04864..05ea8c6f5 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs @@ -21,70 +21,70 @@ public class HighNeedsService(IDatabaseFactory dbFactory) : HighNeedsStubService string[] fields = [ // LocalAuthorityBase - "LaCode AS Code", - "Name", + "LaCode AS [Code]", + "[Name]", // HighNeedsBase - "OutturnTotalHighNeeds AS Total", + "OutturnTotalHighNeeds AS [Total]", // HighNeedsAmount - "OutturnTotalPlaceFunding AS TotalPlaceFunding", - "OutturnTotalTopUpFundingMaintained AS TopUpFundingMaintained", - "OutturnTotalTopUpFundingNonMaintained AS TopUpFundingNonMaintained", - "OutturnTotalSenServices AS SenServices", - "OutturnTotalAlternativeProvisionServices AS AlternativeProvisionServices", - "OutturnTotalHospitalServices AS HospitalServices", - "OutturnTotalOtherHealthServices AS OtherHealthServices", + "OutturnTotalPlaceFunding AS [TotalPlaceFunding]", + "OutturnTotalTopUpFundingMaintained AS [TopUpFundingMaintained]", + "OutturnTotalTopUpFundingNonMaintained AS [TopUpFundingNonMaintained]", + "OutturnTotalSenServices AS [SenServices]", + "OutturnTotalAlternativeProvisionServices AS [AlternativeProvisionServices]", + "OutturnTotalHospitalServices AS [HospitalServices]", + "OutturnTotalOtherHealthServices AS [OtherHealthServices]", // TopFunding - "OutturnTopFundingMaintainedEarlyYears AS EarlyYears", + "OutturnTopFundingMaintainedEarlyYears AS [EarlyYears]", "OutturnTopFundingMaintainedPrimary AS [Primary]", - "OutturnTopFundingMaintainedSecondary AS Secondary", - "OutturnTopFundingMaintainedSpecial AS Special", - "OutturnTopFundingMaintainedAlternativeProvision AS AlternativeProvision", - "OutturnTopFundingMaintainedPostSchool AS PostSchool", - "OutturnTopFundingMaintainedIncome AS Income", + "OutturnTopFundingMaintainedSecondary AS [Secondary]", + "OutturnTopFundingMaintainedSpecial AS [Special]", + "OutturnTopFundingMaintainedAlternativeProvision AS [AlternativeProvision]", + "OutturnTopFundingMaintainedPostSchool AS [PostSchool]", + "OutturnTopFundingMaintainedIncome AS [Income]", // TopFunding - "OutturnTopFundingNonMaintainedEarlyYears AS EarlyYears", + "OutturnTopFundingNonMaintainedEarlyYears AS [EarlyYears]", "OutturnTopFundingNonMaintainedPrimary AS [Primary]", - "OutturnTopFundingNonMaintainedSecondary AS Secondary", - "OutturnTopFundingNonMaintainedSpecial AS Special", - "OutturnTopFundingNonMaintainedAlternativeProvision AS AlternativeProvision", - "OutturnTopFundingNonMaintainedPostSchool AS PostSchool", - "OutturnTopFundingNonMaintainedIncome AS Income", + "OutturnTopFundingNonMaintainedSecondary AS [Secondary]", + "OutturnTopFundingNonMaintainedSpecial AS [Special]", + "OutturnTopFundingNonMaintainedAlternativeProvision AS [AlternativeProvision]", + "OutturnTopFundingNonMaintainedPostSchool AS [PostSchool]", + "OutturnTopFundingNonMaintainedIncome AS [Income]", // PlaceFunding "OutturnPlaceFundingPrimary AS [Primary]", - "OutturnPlaceFundingSecondary AS Secondary", - "OutturnPlaceFundingSpecial AS Special", - "OutturnPlaceFundingAlternativeProvision AS AlternativeProvision", + "OutturnPlaceFundingSecondary AS [Secondary]", + "OutturnPlaceFundingSpecial AS [Special]", + "OutturnPlaceFundingAlternativeProvision AS [AlternativeProvision]", // HighNeedsBase - "BudgetTotalHighNeeds AS Total", + "BudgetTotalHighNeeds AS [Total]", // HighNeedsAmount - "BudgetTotalPlaceFunding AS TotalPlaceFunding", - "BudgetTotalTopUpFundingMaintained AS TopUpFundingMaintained", - "BudgetTotalTopUpFundingNonMaintained AS TopUpFundingNonMaintained", - "BudgetTotalSenServices AS SenServices", - "BudgetTotalAlternativeProvisionServices AS AlternativeProvisionServices", - "BudgetTotalHospitalServices AS HospitalServices", - "BudgetTotalOtherHealthServices AS OtherHealthServices", + "BudgetTotalPlaceFunding AS [TotalPlaceFunding]", + "BudgetTotalTopUpFundingMaintained AS [TopUpFundingMaintained]", + "BudgetTotalTopUpFundingNonMaintained AS [TopUpFundingNonMaintained]", + "BudgetTotalSenServices AS [SenServices]", + "BudgetTotalAlternativeProvisionServices AS [AlternativeProvisionServices]", + "BudgetTotalHospitalServices AS [HospitalServices]", + "BudgetTotalOtherHealthServices AS [OtherHealthServices]", // TopFunding - "BudgetTopFundingMaintainedEarlyYears AS EarlyYears", + "BudgetTopFundingMaintainedEarlyYears AS [EarlyYears]", "BudgetTopFundingMaintainedPrimary AS [Primary]", - "BudgetTopFundingMaintainedSecondary AS Secondary", - "BudgetTopFundingMaintainedSpecial AS Special", - "BudgetTopFundingMaintainedAlternativeProvision AS AlternativeProvision", - "BudgetTopFundingMaintainedPostSchool AS PostSchool", - "BudgetTopFundingMaintainedIncome AS Income", + "BudgetTopFundingMaintainedSecondary AS [Secondary]", + "BudgetTopFundingMaintainedSpecial AS [Special]", + "BudgetTopFundingMaintainedAlternativeProvision AS [AlternativeProvision]", + "BudgetTopFundingMaintainedPostSchool AS [PostSchool]", + "BudgetTopFundingMaintainedIncome AS [Income]", // TopFunding - "BudgetTopFundingNonMaintainedEarlyYears AS EarlyYears", + "BudgetTopFundingNonMaintainedEarlyYears AS [EarlyYears]", "BudgetTopFundingNonMaintainedPrimary AS [Primary]", - "BudgetTopFundingNonMaintainedSecondary AS Secondary", - "BudgetTopFundingNonMaintainedSpecial AS Special", - "BudgetTopFundingNonMaintainedAlternativeProvision AS AlternativeProvision", - "BudgetTopFundingNonMaintainedPostSchool AS PostSchool", - "BudgetTopFundingNonMaintainedIncome AS Income", + "BudgetTopFundingNonMaintainedSecondary AS [Secondary]", + "BudgetTopFundingNonMaintainedSpecial AS [Special]", + "BudgetTopFundingNonMaintainedAlternativeProvision AS [AlternativeProvision]", + "BudgetTopFundingNonMaintainedPostSchool AS [PostSchool]", + "BudgetTopFundingNonMaintainedIncome AS [Income]", // PlaceFunding "BudgetPlaceFundingPrimary AS [Primary]", - "BudgetPlaceFundingSecondary AS Secondary", - "BudgetPlaceFundingSpecial AS Special", - "BudgetPlaceFundingAlternativeProvision AS AlternativeProvision" + "BudgetPlaceFundingSecondary AS [Secondary]", + "BudgetPlaceFundingSpecial AS [Special]", + "BudgetPlaceFundingAlternativeProvision AS [AlternativeProvision]" ]; Type[] types = @@ -120,44 +120,44 @@ public class HighNeedsService(IDatabaseFactory dbFactory) : HighNeedsStubService var laBuilder = new LocalAuthorityCurrentFinancialQuery(dimension, fields) .WhereLaCodesIn(codes); - var results = await conn.QueryAsync(laBuilder, types, Map, splitOn); + var results = await conn.QueryAsync(laBuilder, types, MultiMapToHighNeeds, splitOn); return results.ToArray(); + } - LocalAuthority Map(object[] objects) - { - var localAuthority = objects[0] as LocalAuthorityBase; - var outturn = objects[1] as HighNeedsBase; - var outturnHighNeedsAmount = objects[2] as HighNeedsAmount; - var outturnTopFundingMaintained = objects[3] as TopFunding; - var outturnTopFundingNonMaintained = objects[4] as TopFunding; - var outturnPlaceFunding = objects[5] as PlaceFunding; - var budget = objects[6] as HighNeedsBase; - var budgetHighNeedsAmount = objects[7] as HighNeedsAmount; - var budgeTopFundingMaintained = objects[8] as TopFunding; - var budgetTopFundingNonMaintained = objects[9] as TopFunding; - var budgetPlaceFunding = objects[10] as PlaceFunding; + internal static LocalAuthority MultiMapToHighNeeds(object[] objects) + { + var localAuthority = objects[0] as LocalAuthorityBase; + var outturn = objects[1] as HighNeedsBase; + var outturnHighNeedsAmount = objects[2] as HighNeedsAmount; + var outturnTopFundingMaintained = objects[3] as TopFunding; + var outturnTopFundingNonMaintained = objects[4] as TopFunding; + var outturnPlaceFunding = objects[5] as PlaceFunding; + var budget = objects[6] as HighNeedsBase; + var budgetHighNeedsAmount = objects[7] as HighNeedsAmount; + var budgetTopFundingMaintained = objects[8] as TopFunding; + var budgetTopFundingNonMaintained = objects[9] as TopFunding; + var budgetPlaceFunding = objects[10] as PlaceFunding; - return new LocalAuthority + return new LocalAuthority + { + Code = localAuthority?.Code, + Name = localAuthority?.Name, + Outturn = new Models.HighNeeds + { + Total = outturn?.Total, + HighNeedsAmount = outturnHighNeedsAmount, + Maintained = outturnTopFundingMaintained, + NonMaintained = outturnTopFundingNonMaintained, + PlaceFunding = outturnPlaceFunding + }, + Budget = new Models.HighNeeds { - Code = localAuthority?.Code, - Name = localAuthority?.Name, - Outturn = new Models.HighNeeds - { - Total = outturn?.Total, - HighNeedsAmount = outturnHighNeedsAmount, - Maintained = outturnTopFundingMaintained, - NonMaintained = outturnTopFundingNonMaintained, - PlaceFunding = outturnPlaceFunding - }, - Budget = new Models.HighNeeds - { - Total = budget?.Total, - HighNeedsAmount = budgetHighNeedsAmount, - Maintained = budgeTopFundingMaintained, - NonMaintained = budgetTopFundingNonMaintained, - PlaceFunding = budgetPlaceFunding - } - }; - } + Total = budget?.Total, + HighNeedsAmount = budgetHighNeedsAmount, + Maintained = budgetTopFundingMaintained, + NonMaintained = budgetTopFundingNonMaintained, + PlaceFunding = budgetPlaceFunding + } + }; } } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs index 02a6bc12a..9f80962f1 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsStubService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -12,10 +13,7 @@ namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Services; [SuppressMessage("Performance", "CA1822:Mark members as static")] public class HighNeedsStubService : IHighNeedsService { - public virtual Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default) - { - return Task.FromResult(codes.Select(c => GetLocalAuthority(c, dimension)).ToArray()); - } + public virtual Task[]> Get(string[] codes, string dimension, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public virtual Task?> GetHistory(string[] codes, CancellationToken cancellationToken = default) { @@ -92,21 +90,4 @@ private static IEnumerable GetOutturn(string code, int startYear, AlternativeProvision = baseValue + 25 + year } }; - - private static LocalAuthority GetLocalAuthority(string code, string dimension) - { - if (!int.TryParse(code, out var baseValue)) - { - baseValue = code.Length; - } - - var baseMultiplier = dimension == "Actuals" ? 1_000 : 1; - return new LocalAuthority - { - Code = code, - Name = $"Local authority {code}", - Budget = GetStubbedRow(code, 2024, 1_100 * baseMultiplier + baseValue, 1_110 * baseMultiplier + baseValue % 2), - Outturn = GetStubbedRow(code, 2024, 1_000 * baseMultiplier + baseValue, 1_010 * baseMultiplier + baseValue % 2) - }; - } } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs index 034172dde..154840114 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs @@ -1,8 +1,10 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions; using Microsoft.Extensions.Hosting; using Platform.Api.LocalAuthorityFinances.Configuration; +[assembly: InternalsVisibleTo("Platform.LocalAuthorityFinances.Tests")] var hostBuilder = new HostBuilder() .ConfigureFunctionsWorkerDefaults(Worker.Configure, Worker.Options) .ConfigureServices(Services.Configure) diff --git a/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs new file mode 100644 index 000000000..2d738861d --- /dev/null +++ b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs @@ -0,0 +1,111 @@ +using AutoFixture; +using Moq; +using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; +using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Services; +using Platform.Sql; +using Platform.Sql.QueryBuilders; +using Xunit; + +namespace Platform.LocalAuthorityFinances.Tests.HighNeeds.Services; + +public class WhenHighNeedsServiceQueriesAsync +{ + private readonly Mock _connection; + private readonly Fixture _fixture = new(); + private readonly HighNeedsService _service; + + public WhenHighNeedsServiceQueriesAsync() + { + _connection = new Mock(); + + var dbFactory = new Mock(); + dbFactory.Setup(d => d.GetConnection()).ReturnsAsync(_connection.Object); + + _service = new HighNeedsService(dbFactory.Object); + } + + [Fact] + public async Task ShouldQueryAndMultiMapWhenGetWithValidDimension() + { + // arrange + string[] codes = ["code1", "code2", "code3"]; + const string dimension = "Actuals"; + var results = _fixture + .Build>() + .CreateMany() + .ToArray(); + + string? actualSql = null; + Type[] actualTypes = []; + string[] actualSplitOn = []; + _connection + .Setup(c => c.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny>>(), It.IsAny())) + .Callback>, string[]>((query, types, _, splitOn) => + { + actualSql = query.QueryTemplate.RawSql.Trim(); + actualTypes = types; + actualSplitOn = splitOn; + }) + .ReturnsAsync(results); + + const string expectedSql = "SELECT LaCode AS [Code] , [Name] , OutturnTotalHighNeeds AS [Total] , OutturnTotalPlaceFunding AS [TotalPlaceFunding] , OutturnTotalTopUpFundingMaintained AS [TopUpFundingMaintained] , OutturnTotalTopUpFundingNonMaintained AS [TopUpFundingNonMaintained] , OutturnTotalSenServices AS [SenServices] , OutturnTotalAlternativeProvisionServices AS [AlternativeProvisionServices] , OutturnTotalHospitalServices AS [HospitalServices] , OutturnTotalOtherHealthServices AS [OtherHealthServices] , OutturnTopFundingMaintainedEarlyYears AS [EarlyYears] , OutturnTopFundingMaintainedPrimary AS [Primary] , OutturnTopFundingMaintainedSecondary AS [Secondary] , OutturnTopFundingMaintainedSpecial AS [Special] , OutturnTopFundingMaintainedAlternativeProvision AS [AlternativeProvision] , OutturnTopFundingMaintainedPostSchool AS [PostSchool] , OutturnTopFundingMaintainedIncome AS [Income] , OutturnTopFundingNonMaintainedEarlyYears AS [EarlyYears] , OutturnTopFundingNonMaintainedPrimary AS [Primary] , OutturnTopFundingNonMaintainedSecondary AS [Secondary] , OutturnTopFundingNonMaintainedSpecial AS [Special] , OutturnTopFundingNonMaintainedAlternativeProvision AS [AlternativeProvision] , OutturnTopFundingNonMaintainedPostSchool AS [PostSchool] , OutturnTopFundingNonMaintainedIncome AS [Income] , OutturnPlaceFundingPrimary AS [Primary] , OutturnPlaceFundingSecondary AS [Secondary] , OutturnPlaceFundingSpecial AS [Special] , OutturnPlaceFundingAlternativeProvision AS [AlternativeProvision] , BudgetTotalHighNeeds AS [Total] , BudgetTotalPlaceFunding AS [TotalPlaceFunding] , BudgetTotalTopUpFundingMaintained AS [TopUpFundingMaintained] , BudgetTotalTopUpFundingNonMaintained AS [TopUpFundingNonMaintained] , BudgetTotalSenServices AS [SenServices] , BudgetTotalAlternativeProvisionServices AS [AlternativeProvisionServices] , BudgetTotalHospitalServices AS [HospitalServices] , BudgetTotalOtherHealthServices AS [OtherHealthServices] , BudgetTopFundingMaintainedEarlyYears AS [EarlyYears] , BudgetTopFundingMaintainedPrimary AS [Primary] , BudgetTopFundingMaintainedSecondary AS [Secondary] , BudgetTopFundingMaintainedSpecial AS [Special] , BudgetTopFundingMaintainedAlternativeProvision AS [AlternativeProvision] , BudgetTopFundingMaintainedPostSchool AS [PostSchool] , BudgetTopFundingMaintainedIncome AS [Income] , BudgetTopFundingNonMaintainedEarlyYears AS [EarlyYears] , BudgetTopFundingNonMaintainedPrimary AS [Primary] , BudgetTopFundingNonMaintainedSecondary AS [Secondary] , BudgetTopFundingNonMaintainedSpecial AS [Special] , BudgetTopFundingNonMaintainedAlternativeProvision AS [AlternativeProvision] , BudgetTopFundingNonMaintainedPostSchool AS [PostSchool] , BudgetTopFundingNonMaintainedIncome AS [Income] , BudgetPlaceFundingPrimary AS [Primary] , BudgetPlaceFundingSecondary AS [Secondary] , BudgetPlaceFundingSpecial AS [Special] , BudgetPlaceFundingAlternativeProvision AS [AlternativeProvision]\n FROM VW_LocalAuthorityFinancialDefaultCurrentActual WHERE LaCode IN @LaCodes"; + Type[] expectedTypes = [typeof(LocalAuthorityBase), typeof(HighNeedsBase), typeof(HighNeedsAmount), typeof(TopFunding), typeof(TopFunding), typeof(PlaceFunding), typeof(HighNeedsBase), typeof(HighNeedsAmount), typeof(TopFunding), typeof(TopFunding), typeof(PlaceFunding)]; + string[] expectedSplitOn = ["Total", "TotalPlaceFunding", "EarlyYears", "EarlyYears", "Primary", "Total", "TotalPlaceFunding", "EarlyYears", "EarlyYears", "Primary"]; + + // act + var actual = await _service.Get(codes, dimension); + + // assert + Assert.Equal(results, actual); + Assert.Equal(expectedSql, actualSql); + Assert.Equal(expectedTypes, actualTypes); + Assert.Equal(expectedSplitOn, actualSplitOn); + } + + [Fact] + public void ShouldMapWhenMultiMapToHighNeeds() + { + var localAuthority = _fixture.Create(); + var outturn = _fixture.Create(); + var outturnHighNeedsAmount = _fixture.Create(); + var outturnTopFundingMaintained = _fixture.Create(); + var outturnTopFundingNonMaintained = _fixture.Create(); + var outturnPlaceFunding = _fixture.Create(); + var budget = _fixture.Create(); + var budgetHighNeedsAmount = _fixture.Create(); + var budgetTopFundingMaintained = _fixture.Create(); + var budgetTopFundingNonMaintained = _fixture.Create(); + var budgetPlaceFunding = _fixture.Create(); + object[] objects = + [ + localAuthority, + outturn, + outturnHighNeedsAmount, + outturnTopFundingMaintained, + outturnTopFundingNonMaintained, + outturnPlaceFunding, + budget, + budgetHighNeedsAmount, + budgetTopFundingMaintained, + budgetTopFundingNonMaintained, + budgetPlaceFunding + ]; + + // act + var actual = HighNeedsService.MultiMapToHighNeeds(objects); + + // assert + Assert.Equal(localAuthority.Code, actual.Code); + Assert.Equal(localAuthority.Name, actual.Name); + Assert.Equal(outturn.Total, actual.Outturn?.Total); + Assert.Equal(outturnHighNeedsAmount, actual.Outturn?.HighNeedsAmount); + Assert.Equal(outturnTopFundingMaintained, actual.Outturn?.Maintained); + Assert.Equal(outturnTopFundingNonMaintained, actual.Outturn?.NonMaintained); + Assert.Equal(outturnPlaceFunding, actual.Outturn?.PlaceFunding); + Assert.Equal(budget.Total, actual.Budget?.Total); + Assert.Equal(budgetHighNeedsAmount, actual.Budget?.HighNeedsAmount); + Assert.Equal(budgetTopFundingMaintained, actual.Budget?.Maintained); + Assert.Equal(budgetTopFundingNonMaintained, actual.Budget?.NonMaintained); + Assert.Equal(budgetPlaceFunding, actual.Budget?.PlaceFunding); + } +} \ No newline at end of file From 41273e5c4af8ec37a01568a68749992a0f2bd697 Mon Sep 17 00:00:00 2001 From: Marc Woolfson Date: Fri, 14 Mar 2025 12:52:57 +0000 Subject: [PATCH 3/3] chore: Moved `MultiMapToHighNeeds` function to its own `Mapper` class --- .../Features/HighNeeds/Mapper.cs | 43 ++++++++++++++ .../HighNeeds/Services/HighNeedsService.cs | 39 +----------- .../Program.cs | 2 - .../WhenHighNeedsServiceQueriesAsync.cs | 47 --------------- .../HighNeeds/WhenHighNeedsMapperMaps.cs | 59 +++++++++++++++++++ 5 files changed, 103 insertions(+), 87 deletions(-) create mode 100644 platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Mapper.cs create mode 100644 platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/WhenHighNeedsMapperMaps.cs diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Mapper.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Mapper.cs new file mode 100644 index 000000000..77ba565f2 --- /dev/null +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Mapper.cs @@ -0,0 +1,43 @@ +using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; + +namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds; + +public static class Mapper +{ + public static LocalAuthority MultiMapToHighNeeds(object[] objects) + { + var localAuthority = objects[0] as LocalAuthorityBase; + var outturn = objects[1] as HighNeedsBase; + var outturnHighNeedsAmount = objects[2] as HighNeedsAmount; + var outturnTopFundingMaintained = objects[3] as TopFunding; + var outturnTopFundingNonMaintained = objects[4] as TopFunding; + var outturnPlaceFunding = objects[5] as PlaceFunding; + var budget = objects[6] as HighNeedsBase; + var budgetHighNeedsAmount = objects[7] as HighNeedsAmount; + var budgetTopFundingMaintained = objects[8] as TopFunding; + var budgetTopFundingNonMaintained = objects[9] as TopFunding; + var budgetPlaceFunding = objects[10] as PlaceFunding; + + return new LocalAuthority + { + Code = localAuthority?.Code, + Name = localAuthority?.Name, + Outturn = new Models.HighNeeds + { + Total = outturn?.Total, + HighNeedsAmount = outturnHighNeedsAmount, + Maintained = outturnTopFundingMaintained, + NonMaintained = outturnTopFundingNonMaintained, + PlaceFunding = outturnPlaceFunding + }, + Budget = new Models.HighNeeds + { + Total = budget?.Total, + HighNeedsAmount = budgetHighNeedsAmount, + Maintained = budgetTopFundingMaintained, + NonMaintained = budgetTopFundingNonMaintained, + PlaceFunding = budgetPlaceFunding + } + }; + } +} \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs index 05ea8c6f5..01ba55d3a 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Features/HighNeeds/Services/HighNeedsService.cs @@ -120,44 +120,7 @@ public class HighNeedsService(IDatabaseFactory dbFactory) : HighNeedsStubService var laBuilder = new LocalAuthorityCurrentFinancialQuery(dimension, fields) .WhereLaCodesIn(codes); - var results = await conn.QueryAsync(laBuilder, types, MultiMapToHighNeeds, splitOn); + var results = await conn.QueryAsync(laBuilder, types, Mapper.MultiMapToHighNeeds, splitOn); return results.ToArray(); } - - internal static LocalAuthority MultiMapToHighNeeds(object[] objects) - { - var localAuthority = objects[0] as LocalAuthorityBase; - var outturn = objects[1] as HighNeedsBase; - var outturnHighNeedsAmount = objects[2] as HighNeedsAmount; - var outturnTopFundingMaintained = objects[3] as TopFunding; - var outturnTopFundingNonMaintained = objects[4] as TopFunding; - var outturnPlaceFunding = objects[5] as PlaceFunding; - var budget = objects[6] as HighNeedsBase; - var budgetHighNeedsAmount = objects[7] as HighNeedsAmount; - var budgetTopFundingMaintained = objects[8] as TopFunding; - var budgetTopFundingNonMaintained = objects[9] as TopFunding; - var budgetPlaceFunding = objects[10] as PlaceFunding; - - return new LocalAuthority - { - Code = localAuthority?.Code, - Name = localAuthority?.Name, - Outturn = new Models.HighNeeds - { - Total = outturn?.Total, - HighNeedsAmount = outturnHighNeedsAmount, - Maintained = outturnTopFundingMaintained, - NonMaintained = outturnTopFundingNonMaintained, - PlaceFunding = outturnPlaceFunding - }, - Budget = new Models.HighNeeds - { - Total = budget?.Total, - HighNeedsAmount = budgetHighNeedsAmount, - Maintained = budgetTopFundingMaintained, - NonMaintained = budgetTopFundingNonMaintained, - PlaceFunding = budgetPlaceFunding - } - }; - } } \ No newline at end of file diff --git a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs index 154840114..034172dde 100644 --- a/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs +++ b/platform/src/apis/Platform.Api.LocalAuthorityFinances/Program.cs @@ -1,10 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions; using Microsoft.Extensions.Hosting; using Platform.Api.LocalAuthorityFinances.Configuration; -[assembly: InternalsVisibleTo("Platform.LocalAuthorityFinances.Tests")] var hostBuilder = new HostBuilder() .ConfigureFunctionsWorkerDefaults(Worker.Configure, Worker.Options) .ConfigureServices(Services.Configure) diff --git a/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs index 2d738861d..046393cb5 100644 --- a/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs +++ b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/Services/WhenHighNeedsServiceQueriesAsync.cs @@ -61,51 +61,4 @@ public async Task ShouldQueryAndMultiMapWhenGetWithValidDimension() Assert.Equal(expectedTypes, actualTypes); Assert.Equal(expectedSplitOn, actualSplitOn); } - - [Fact] - public void ShouldMapWhenMultiMapToHighNeeds() - { - var localAuthority = _fixture.Create(); - var outturn = _fixture.Create(); - var outturnHighNeedsAmount = _fixture.Create(); - var outturnTopFundingMaintained = _fixture.Create(); - var outturnTopFundingNonMaintained = _fixture.Create(); - var outturnPlaceFunding = _fixture.Create(); - var budget = _fixture.Create(); - var budgetHighNeedsAmount = _fixture.Create(); - var budgetTopFundingMaintained = _fixture.Create(); - var budgetTopFundingNonMaintained = _fixture.Create(); - var budgetPlaceFunding = _fixture.Create(); - object[] objects = - [ - localAuthority, - outturn, - outturnHighNeedsAmount, - outturnTopFundingMaintained, - outturnTopFundingNonMaintained, - outturnPlaceFunding, - budget, - budgetHighNeedsAmount, - budgetTopFundingMaintained, - budgetTopFundingNonMaintained, - budgetPlaceFunding - ]; - - // act - var actual = HighNeedsService.MultiMapToHighNeeds(objects); - - // assert - Assert.Equal(localAuthority.Code, actual.Code); - Assert.Equal(localAuthority.Name, actual.Name); - Assert.Equal(outturn.Total, actual.Outturn?.Total); - Assert.Equal(outturnHighNeedsAmount, actual.Outturn?.HighNeedsAmount); - Assert.Equal(outturnTopFundingMaintained, actual.Outturn?.Maintained); - Assert.Equal(outturnTopFundingNonMaintained, actual.Outturn?.NonMaintained); - Assert.Equal(outturnPlaceFunding, actual.Outturn?.PlaceFunding); - Assert.Equal(budget.Total, actual.Budget?.Total); - Assert.Equal(budgetHighNeedsAmount, actual.Budget?.HighNeedsAmount); - Assert.Equal(budgetTopFundingMaintained, actual.Budget?.Maintained); - Assert.Equal(budgetTopFundingNonMaintained, actual.Budget?.NonMaintained); - Assert.Equal(budgetPlaceFunding, actual.Budget?.PlaceFunding); - } } \ No newline at end of file diff --git a/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/WhenHighNeedsMapperMaps.cs b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/WhenHighNeedsMapperMaps.cs new file mode 100644 index 000000000..345ae2eff --- /dev/null +++ b/platform/tests/Platform.LocalAuthorityFinances.Tests/HighNeeds/WhenHighNeedsMapperMaps.cs @@ -0,0 +1,59 @@ +using AutoFixture; +using Platform.Api.LocalAuthorityFinances.Features.HighNeeds; +using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models; +using Xunit; + +namespace Platform.LocalAuthorityFinances.Tests.HighNeeds; + +public class WhenHighNeedsMapperMaps +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void ShouldMapWhenMultiMapToHighNeeds() + { + // arrange + var localAuthority = _fixture.Create(); + var outturn = _fixture.Create(); + var outturnHighNeedsAmount = _fixture.Create(); + var outturnTopFundingMaintained = _fixture.Create(); + var outturnTopFundingNonMaintained = _fixture.Create(); + var outturnPlaceFunding = _fixture.Create(); + var budget = _fixture.Create(); + var budgetHighNeedsAmount = _fixture.Create(); + var budgetTopFundingMaintained = _fixture.Create(); + var budgetTopFundingNonMaintained = _fixture.Create(); + var budgetPlaceFunding = _fixture.Create(); + object[] objects = + [ + localAuthority, + outturn, + outturnHighNeedsAmount, + outturnTopFundingMaintained, + outturnTopFundingNonMaintained, + outturnPlaceFunding, + budget, + budgetHighNeedsAmount, + budgetTopFundingMaintained, + budgetTopFundingNonMaintained, + budgetPlaceFunding + ]; + + // act + var actual = Mapper.MultiMapToHighNeeds(objects); + + // assert + Assert.Equal(localAuthority.Code, actual.Code); + Assert.Equal(localAuthority.Name, actual.Name); + Assert.Equal(outturn.Total, actual.Outturn?.Total); + Assert.Equal(outturnHighNeedsAmount, actual.Outturn?.HighNeedsAmount); + Assert.Equal(outturnTopFundingMaintained, actual.Outturn?.Maintained); + Assert.Equal(outturnTopFundingNonMaintained, actual.Outturn?.NonMaintained); + Assert.Equal(outturnPlaceFunding, actual.Outturn?.PlaceFunding); + Assert.Equal(budget.Total, actual.Budget?.Total); + Assert.Equal(budgetHighNeedsAmount, actual.Budget?.HighNeedsAmount); + Assert.Equal(budgetTopFundingMaintained, actual.Budget?.Maintained); + Assert.Equal(budgetTopFundingNonMaintained, actual.Budget?.NonMaintained); + Assert.Equal(budgetPlaceFunding, actual.Budget?.PlaceFunding); + } +} \ No newline at end of file