Skip to content

Commit 609374d

Browse files
Merge branch 'main' into DTOSS-12664-Filter-health-alerts-from-logging
2 parents 3b472c4 + 1364ccb commit 609374d

8 files changed

Lines changed: 288 additions & 7 deletions

File tree

application/CohortManager/src/Functions/CaasIntegration/receiveCaasFile/receiveCaasFile.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ private async Task EnqueueAuditMessagesAsync(
149149
RecordSourceDesc = $"Parquet file: {fileName}",
150150
CreatedBy = nameof(ReceiveCaasFile),
151151
ScreeningId = screeningId,
152+
RequestSnapshot = participant,
152153
});
153154

154155
var failCount = await _auditLogClient.AddBatchAsync(auditMessages);

application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/AllocateServiceProvider/AllocationConfigDataList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ namespace NHS.CohortManager.CohortDistributionServices;
55
public class AllocationConfigDataList
66
{
77
[JsonPropertyName("allocation_config_data_list")]
8-
public AllocationConfigData[]? ConfigDataList { get; set; }
8+
public AllocationConfigData[] ConfigDataList { get; set; } = [];
99

1010
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace NHS.CohortManager.CohortDistributionServices;
2+
3+
using System.Text.Json;
4+
using Microsoft.Extensions.Logging;
5+
6+
/// <summary>
7+
/// Provides cached access to the allocation config data.
8+
/// Registered as a singleton so the file is read once and reused across calls.
9+
/// Config is loaded eagerly in the constructor and the provider does not mutate it after creation.
10+
/// Callers receive the cached instance returned by reference, so this type does not guarantee that the config object itself is immutable.
11+
/// </summary>
12+
public class AllocationConfigProvider : IAllocationConfigProvider
13+
{
14+
private readonly AllocationConfigDataList _cachedConfig;
15+
16+
public AllocationConfigProvider(ILogger<AllocationConfigProvider> logger)
17+
{
18+
var configFilePath = Path.Combine(Environment.CurrentDirectory, "AllocateServiceProvider", "allocationConfig.json");
19+
var configFile = File.ReadAllText(configFilePath);
20+
_cachedConfig = JsonSerializer.Deserialize<AllocationConfigDataList>(configFile)
21+
?? throw new InvalidOperationException($"Failed to deserialize allocation config from '{configFilePath}'. The file content was null or invalid.");
22+
logger.LogInformation("Allocation config loaded from file and cached");
23+
}
24+
25+
public AllocationConfigDataList GetConfig() => _cachedConfig;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace NHS.CohortManager.CohortDistributionServices;
2+
3+
public interface IAllocationConfigProvider
4+
{
5+
AllocationConfigDataList GetConfig();
6+
}

application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/DistributeParticipantActivities.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,23 @@ public class DistributeParticipantActivities
1717
private readonly DistributeParticipantConfig _config;
1818
private readonly ILogger<DistributeParticipantActivities> _logger;
1919
private readonly IHttpClientFunction _httpClientFunction;
20+
private readonly IAllocationConfigProvider _allocationConfigProvider;
2021

2122
public DistributeParticipantActivities(IDataServiceClient<CohortDistribution> cohortDistributionClient,
2223
IDataServiceClient<ParticipantManagement> participantManagementClient,
2324
IDataServiceClient<ParticipantDemographic> participantDemographicClient,
2425
IOptions<DistributeParticipantConfig> config,
2526
ILogger<DistributeParticipantActivities> logger,
26-
IHttpClientFunction httpClientFunction)
27+
IHttpClientFunction httpClientFunction,
28+
IAllocationConfigProvider allocationConfigProvider)
2729
{
2830
_cohortDistributionClient = cohortDistributionClient;
2931
_participantManagementClient = participantManagementClient;
3032
_participantDemographicClient = participantDemographicClient;
3133
_config = config.Value;
3234
_logger = logger;
3335
_httpClientFunction = httpClientFunction;
36+
_allocationConfigProvider = allocationConfigProvider;
3437
}
3538

3639
/// <summary>
@@ -87,11 +90,7 @@ public async Task<string> AllocateServiceProvider([ActivityTrigger] CohortDistri
8790
return EnumHelper.GetDisplayName(ServiceProvider.BSS);
8891
}
8992

90-
//TODO: This should be pre-cached rather than read from file every time. This is fine for now as the file is small and this function is not called frequently, but if this becomes a bottleneck we should look at caching the config data in memory and refreshing it periodically instead of reading from file on every function call.
91-
string configFilePath = Path.Combine(Environment.CurrentDirectory, "AllocateServiceProvider", "allocationConfig.json");
92-
string configFile = await File.ReadAllTextAsync(configFilePath);
93-
94-
var allocationConfigEntries = JsonSerializer.Deserialize<AllocationConfigDataList>(configFile);
93+
var allocationConfigEntries = _allocationConfigProvider.GetConfig();
9594

9695
string serviceProvider = allocationConfigEntries.ConfigDataList
9796
.Where(item => participant.Postcode.StartsWith(item.Postcode, StringComparison.OrdinalIgnoreCase) &&

application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
{
1919
// Register health checks
2020
services.AddBasicHealthCheck("DistributeParticipant");
21+
services.AddSingleton<IAllocationConfigProvider, AllocationConfigProvider>();
2122
})
2223
.AddExceptionHandler()
2324
.AddTelemetry()

application/CohortManager/src/Functions/ParticipantManagementServices/ManageServiceNowParticipant/ManageServiceNowParticipantFunction.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ await _auditLogClient.AddAsync(new ParticipantAuditMessage
9393
RecordSourceDesc = $"ServiceNow case: {serviceNowParticipant.ServiceNowCaseNumber}",
9494
CreatedBy = nameof(ManageServiceNowParticipantFunction),
9595
ScreeningId = (int)serviceNowParticipant.ScreeningId,
96+
RequestSnapshot = serviceNowParticipant,
9697
});
9798
}
9899
catch (Exception ex)
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
namespace NHS.CohortManager.Tests.CohortDistributionServiceTests;
2+
3+
using DataServices.Client;
4+
using Microsoft.Extensions.Logging.Abstractions;
5+
using Microsoft.Extensions.Options;
6+
using Model;
7+
using Model.Enums;
8+
using Moq;
9+
using NHS.CohortManager.CohortDistributionServices;
10+
using Common;
11+
12+
[TestClass]
13+
public class AllocateServiceProviderTests
14+
{
15+
private readonly DistributeParticipantActivities _distributionParticipantActivities;
16+
private readonly Mock<IAllocationConfigProvider> _allocationConfigProvider = new();
17+
private readonly Mock<IDataServiceClient<CohortDistribution>> _cohortDistributionClient = new();
18+
private readonly Mock<IDataServiceClient<ParticipantManagement>> _participantManagementClient = new();
19+
private readonly Mock<IDataServiceClient<ParticipantDemographic>> _participantDemographicClient = new();
20+
private readonly Mock<IHttpClientFunction> _httpClientFunction = new();
21+
private readonly Mock<IOptions<DistributeParticipantConfig>> _config = new();
22+
23+
public AllocateServiceProviderTests()
24+
{
25+
DistributeParticipantConfig config = new()
26+
{
27+
LookupValidationURL = "LookupValidationURL",
28+
StaticValidationURL = "StaticValidationURL",
29+
TransformDataServiceURL = "TransformDataServiceURL",
30+
ParticipantManagementUrl = "ParticipantManagementUrl",
31+
CohortDistributionDataServiceUrl = "CohortDistributionDataServiceUrl",
32+
ParticipantDemographicDataServiceUrl = "ParticipantDemographicDataServiceUrl",
33+
CohortDistributionTopic = "cohort-distribution-topic",
34+
DistributeParticipantSubscription = "distribute-participant-sub",
35+
RemoveOldValidationRecordUrl = "RemoveOldValidationRecordUrl",
36+
SendServiceNowMessageURL = "SendServiceNowMessageURL"
37+
};
38+
39+
_config.Setup(x => x.Value).Returns(config);
40+
41+
_distributionParticipantActivities = new(
42+
_cohortDistributionClient.Object,
43+
_participantManagementClient.Object,
44+
_participantDemographicClient.Object,
45+
_config.Object,
46+
NullLogger<DistributeParticipantActivities>.Instance,
47+
_httpClientFunction.Object,
48+
_allocationConfigProvider.Object
49+
);
50+
}
51+
52+
53+
[TestMethod]
54+
public async Task AllocateServiceProvider_PostcodeIsNull_ReturnsBSSDefault()
55+
{
56+
// Arrange
57+
var participant = new CohortDistributionParticipant { Postcode = null, ScreeningAcronym = "BSS" };
58+
59+
// Act
60+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
61+
62+
// Assert
63+
Assert.AreEqual(EnumHelper.GetDisplayName(ServiceProvider.BSS), result);
64+
}
65+
66+
[TestMethod]
67+
public async Task AllocateServiceProvider_PostcodeIsEmpty_ReturnsBSSDefault()
68+
{
69+
// Arrange
70+
var participant = new CohortDistributionParticipant { Postcode = "", ScreeningAcronym = "BSS" };
71+
72+
// Act
73+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
74+
75+
// Assert
76+
Assert.AreEqual(EnumHelper.GetDisplayName(ServiceProvider.BSS), result);
77+
}
78+
79+
[TestMethod]
80+
public async Task AllocateServiceProvider_ScreeningAcronymIsNull_ReturnsBSSDefault()
81+
{
82+
// Arrange
83+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = null };
84+
85+
// Act
86+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
87+
88+
// Assert
89+
Assert.AreEqual(EnumHelper.GetDisplayName(ServiceProvider.BSS), result);
90+
}
91+
92+
[TestMethod]
93+
public async Task AllocateServiceProvider_ScreeningAcronymIsEmpty_ReturnsBSSDefault()
94+
{
95+
// Arrange
96+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "" };
97+
98+
// Act
99+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
100+
101+
// Assert
102+
Assert.AreEqual(EnumHelper.GetDisplayName(ServiceProvider.BSS), result);
103+
}
104+
105+
[TestMethod]
106+
public async Task AllocateServiceProvider_MatchingPostcodeAndScreeningService_ReturnsConfiguredProvider()
107+
{
108+
// Arrange
109+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "BSS" };
110+
var configData = new AllocationConfigDataList
111+
{
112+
ConfigDataList =
113+
[
114+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "TestServiceProvider" }
115+
]
116+
};
117+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
118+
119+
// Act
120+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
121+
122+
// Assert
123+
Assert.AreEqual("TestServiceProvider", result);
124+
}
125+
126+
[TestMethod]
127+
public async Task AllocateServiceProvider_NoMatchingPostcode_ReturnsBSSelect()
128+
{
129+
// Arrange
130+
var participant = new CohortDistributionParticipant { Postcode = "ZZ1 2CD", ScreeningAcronym = "BSS" };
131+
var configData = new AllocationConfigDataList
132+
{
133+
ConfigDataList =
134+
[
135+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "TestServiceProvider" }
136+
]
137+
};
138+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
139+
140+
// Act
141+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
142+
143+
// Assert
144+
Assert.AreEqual("BS SELECT", result);
145+
}
146+
147+
[TestMethod]
148+
public async Task AllocateServiceProvider_NoMatchingScreeningService_ReturnsBSSelect()
149+
{
150+
// Arrange
151+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "CSS" };
152+
var configData = new AllocationConfigDataList
153+
{
154+
ConfigDataList =
155+
[
156+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "TestServiceProvider" }
157+
]
158+
};
159+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
160+
161+
// Act
162+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
163+
164+
// Assert
165+
Assert.AreEqual("BS SELECT", result);
166+
}
167+
168+
[TestMethod]
169+
public async Task AllocateServiceProvider_MultipleMatches_ReturnsLongestPostcodeMatch()
170+
{
171+
// Arrange
172+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "BSS" };
173+
var configData = new AllocationConfigDataList
174+
{
175+
ConfigDataList =
176+
[
177+
new AllocationConfigData { Postcode = "A", ScreeningService = "BSS", ServiceProvider = "Short Match" },
178+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "Longer Match" },
179+
new AllocationConfigData { Postcode = "AB1", ScreeningService = "BSS", ServiceProvider = "Longest Match" }
180+
]
181+
};
182+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
183+
184+
// Act
185+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
186+
187+
// Assert
188+
Assert.AreEqual("Longest Match", result);
189+
}
190+
191+
[TestMethod]
192+
public async Task AllocateServiceProvider_PostcodeMatchIsCaseInsensitive_ReturnsMatch()
193+
{
194+
// Arrange
195+
var participant = new CohortDistributionParticipant { Postcode = "ab1 2cd", ScreeningAcronym = "BSS" };
196+
var configData = new AllocationConfigDataList
197+
{
198+
ConfigDataList =
199+
[
200+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "TestServiceProvider" }
201+
]
202+
};
203+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
204+
205+
// Act
206+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
207+
208+
// Assert
209+
Assert.AreEqual("TestServiceProvider", result);
210+
}
211+
212+
[TestMethod]
213+
public async Task AllocateServiceProvider_ScreeningAcronymMatchIsCaseInsensitive_ReturnsMatch()
214+
{
215+
// Arrange
216+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "bss" };
217+
var configData = new AllocationConfigDataList
218+
{
219+
ConfigDataList =
220+
[
221+
new AllocationConfigData { Postcode = "AB", ScreeningService = "BSS", ServiceProvider = "TestServiceProvider" }
222+
]
223+
};
224+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
225+
226+
// Act
227+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
228+
229+
// Assert
230+
Assert.AreEqual("TestServiceProvider", result);
231+
}
232+
233+
[TestMethod]
234+
public async Task AllocateServiceProvider_EmptyConfigDataList_ReturnsBSSelect()
235+
{
236+
// Arrange
237+
var participant = new CohortDistributionParticipant { Postcode = "AB1 2CD", ScreeningAcronym = "BSS" };
238+
var configData = new AllocationConfigDataList { ConfigDataList = [] };
239+
_allocationConfigProvider.Setup(x => x.GetConfig()).Returns(configData);
240+
241+
// Act
242+
var result = await _distributionParticipantActivities.AllocateServiceProvider(participant);
243+
244+
// Assert
245+
Assert.AreEqual("BS SELECT", result);
246+
}
247+
}

0 commit comments

Comments
 (0)