Skip to content

Commit 2e9ceec

Browse files
authored
Merge branch 'main' into fix/DTOSS-10107-fix-poscode-validation-rule
2 parents 829ccc7 + e0f7e3d commit 2e9ceec

27 files changed

Lines changed: 1126 additions & 255 deletions

File tree

application/CohortManager/compose.core.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ services:
140140
- ParticipantManagementUrl=http://participant-management-data-service:7994/api/ParticipantManagementDataService
141141
- ParticipantDemographicDataServiceURL=http://participant-demographic-data-service:7993/api/ParticipantDemographicDataService/
142142
- ExceptionFunctionURL=http://create-exception:7070/api/CreateException
143+
- ManageNemsSubscriptionUnsubscribeURL=http://manage-nems-subscription:9081/api/Unsubscribe
144+
- ManageNemsSubscriptionSubscribeURL=http://manage-nems-subscription:9081/api/ManageNemsSubscriptionSubscribeURL
145+
- RetrievePdsDemographicURL=http://etrieve-pds-demographic:8082/api/RetrievePdsDemographic
143146

144147
delete-participant:
145148
container_name: delete-participant

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ public class ReceiveCaasFileConfig
1919
public string caasfolder_STORAGE { get; set; }
2020
[Required]
2121
public string inboundBlobName { get; set; }
22+
[Required]
2223
public string ServiceBusConnectionString_client_internal { get; set; }
2324
public string GetOrchestrationStatusURL { get; set; }
25+
[Required]
2426
public string ParticipantManagementTopic { get; set; }
2527
}

application/CohortManager/src/Functions/DemographicServices/ManageNemsSubscription/Extensions/CertificateExtensions.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
namespace NHS.CohortManager.DemographicServices;
2+
13
using System.Security.Cryptography.X509Certificates;
24
using Azure.Identity;
35
using Azure.Security.KeyVault.Certificates;
46
using Microsoft.Extensions.Logging;
57

6-
namespace NHS.CohortManager.DemographicServices;
8+
79

810
public static class CertificateExtensions
911
{
@@ -26,15 +28,16 @@ public static async Task<X509Certificate2> LoadNemsCertificateAsync(this ManageN
2628
var certResult = await certClient.DownloadCertificateAsync(config.NemsKeyName);
2729
return certResult.Value;
2830
}
29-
31+
3032
if (!string.IsNullOrEmpty(config.NemsLocalCertPath))
3133
{
3234
logger.LogInformation("Loading NEMS certificate from local file");
3335
return !string.IsNullOrEmpty(config.NemsLocalCertPassword)
3436
? new X509Certificate2(config.NemsLocalCertPath, config.NemsLocalCertPassword)
3537
: new X509Certificate2(config.NemsLocalCertPath);
38+
3639
}
37-
40+
3841
throw new InvalidOperationException("No certificate configuration found. Please configure either KeyVaultConnectionString or NemsLocalCertPath.");
3942
}
40-
}
43+
}

application/CohortManager/src/Functions/DemographicServices/ManageNemsSubscription/ManageNemsSubscriptionConfig.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,6 @@ public class ManageNemsSubscriptionConfig
8383
/// </summary>
8484
public bool NemsBypassServerCertificateValidation { get; set; } = false;
8585

86-
/// <summary>
87-
/// Default event types to subscribe to
88-
/// </summary>
89-
public string[] NemsDefaultEventTypes { get; set; } = new[]
90-
{
91-
"pds-record-change-1"
92-
};
93-
9486
/// <summary>
9587
/// HTTP client timeout in seconds for NEMS API requests
9688
/// Default: 300 seconds (5 minutes)
@@ -101,5 +93,9 @@ public class ManageNemsSubscriptionConfig
10193
/// Custom validation to ensure either KeyVault or local cert is configured
10294
/// </summary>
10395
public bool IsValid => !string.IsNullOrEmpty(KeyVaultConnectionString) || !string.IsNullOrEmpty(NemsLocalCertPath);
96+
/// <summary>
97+
/// Bool to set the function to be in stubbed mode. Simulated responses from NEMS
98+
/// </summary>
99+
public bool IsStubbed { get; set; } = false;
104100
}
105101

application/CohortManager/src/Functions/DemographicServices/ManageNemsSubscription/Program.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,37 @@
66
using NHS.CohortManager.DemographicServices;
77
using DataServices.Database;
88
using Microsoft.Extensions.Logging;
9+
using System.Security.Cryptography.X509Certificates;
910

1011
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
1112
var logger = loggerFactory.CreateLogger("Program");
1213

1314
var host = new HostBuilder();
1415

16+
17+
logger.LogInformation("Application Has Started");
1518
// Load configuration
1619
host.AddConfiguration<ManageNemsSubscriptionConfig>(out ManageNemsSubscriptionConfig config);
1720

1821
var nemsConfig = config;
1922

23+
X509Certificate2 nemsCertificate;
24+
25+
2026
// Load NEMS certificate up-front and inject into DI
21-
var nemsCertificate = await nemsConfig.LoadNemsCertificateAsync(logger);
27+
28+
nemsCertificate = await nemsConfig.LoadNemsCertificateAsync(logger);
29+
2230

2331
host.ConfigureFunctionsWebApplication();
2432
host.AddHttpClient()
25-
.AddNemsHttpClient()
33+
.AddNemsHttpClient(nemsConfig.IsStubbed)
2634
.ConfigureServices(services =>
2735
{
2836
// Register NEMS certificate
2937
services.AddSingleton(nemsCertificate);
3038

39+
3140
// Register NEMS subscription manager
3241
services.AddScoped<NemsSubscriptionManager>();
3342

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace NHS.CohortManager.ParticipantManagementService;
2+
3+
public class BlockParticipantDto
4+
{
5+
public required long NhsNumber { get; set; }
6+
public required string DateOfBirth { get; set; }
7+
public required string FamilyName { get; set; }
8+
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
namespace NHS.CohortManager.ParticipantManagementService;
2+
3+
using System;
4+
using System.Globalization;
5+
using System.Text.Json;
6+
using Common;
7+
using DataServices.Client;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Options;
10+
using Model;
11+
12+
public class BlockParticipantHandler : IBlockParticipantHandler
13+
{
14+
private readonly ILogger<BlockParticipantHandler> _logger;
15+
private readonly IDataServiceClient<ParticipantManagement> _participantManagementDataService;
16+
private readonly IDataServiceClient<ParticipantDemographic> _participantDemographicDataService;
17+
private readonly IHttpClientFunction _httpClient;
18+
private readonly UpdateBlockedFlagConfig _config;
19+
public BlockParticipantHandler(ILogger<BlockParticipantHandler> logger,
20+
IDataServiceClient<ParticipantManagement> participantManagementDataService,
21+
IDataServiceClient<ParticipantDemographic> participantDemographicDataService,
22+
IHttpClientFunction httpClient,
23+
IOptions<UpdateBlockedFlagConfig> config
24+
)
25+
{
26+
_logger = logger;
27+
_participantManagementDataService = participantManagementDataService;
28+
_participantDemographicDataService = participantDemographicDataService;
29+
_httpClient = httpClient;
30+
_config = config.Value;
31+
}
32+
33+
public async Task<BlockParticipantResult> BlockParticipant(BlockParticipantDto blockParticipantRequest)
34+
{
35+
if (!ValidationHelper.ValidateNHSNumber(blockParticipantRequest.NhsNumber.ToString()))
36+
{
37+
_logger.LogWarning("Participant had an invalid NHS Number and cannot be blocked");
38+
return new BlockParticipantResult(false, "Invalid NHS Number");
39+
}
40+
41+
42+
43+
var participantManagementRecord = await _participantManagementDataService.GetSingleByFilter(x => x.NHSNumber == blockParticipantRequest.NhsNumber);
44+
45+
if (participantManagementRecord == null)
46+
{
47+
return await BlockNewParticipant(blockParticipantRequest);
48+
}
49+
50+
if (participantManagementRecord.BlockedFlag == 1)
51+
{
52+
_logger.LogWarning("Participant already blocked and cannot be blocked");
53+
return new BlockParticipantResult(false, "Participant Already Blocked");
54+
}
55+
56+
var participantDemographic = await _participantDemographicDataService.GetSingleByFilter(x => x.NhsNumber == blockParticipantRequest.NhsNumber);
57+
58+
if (!ValidateRecordsMatch(participantDemographic, blockParticipantRequest))
59+
{
60+
_logger.LogWarning("Participant didn't pass three point check and cannot be blocked");
61+
return new BlockParticipantResult(false, "Participant Didn't pass three point check");
62+
}
63+
64+
_logger.LogInformation("Participant has been blocked");
65+
return await BlockExistingParticipant(participantManagementRecord);
66+
67+
68+
69+
}
70+
71+
public async Task<BlockParticipantResult> UnblockParticipant(long nhsNumber)
72+
{
73+
74+
var participantManagementRecord = await _participantManagementDataService.GetSingleByFilter(x => x.NHSNumber == nhsNumber);
75+
76+
if (participantManagementRecord == null)
77+
{
78+
return new BlockParticipantResult(false, "Participant Couldn't be found");
79+
}
80+
81+
if (participantManagementRecord.BlockedFlag != 1)
82+
{
83+
_logger.LogInformation("Participant couldn't be unblocked as they are not currently blocked");
84+
return new BlockParticipantResult(false, "Participant is not blocked");
85+
}
86+
87+
88+
var blockedFlagSet = await SetBlockedFlag(participantManagementRecord, false);
89+
if (!blockedFlagSet)
90+
{
91+
return new BlockParticipantResult(false, "Failed to unset blocked flag");
92+
}
93+
94+
if (participantManagementRecord.EligibilityFlag == 0)
95+
{
96+
return new BlockParticipantResult(true, "Participant was unblocked but not resubscribed to Nems as they are ineligible");
97+
}
98+
99+
var nemsSubscribed = await SubscribeParticipantToNEMS(nhsNumber);
100+
if (!nemsSubscribed)
101+
{
102+
return new BlockParticipantResult(false, "Participant couldn't be subscribed in Nems");
103+
}
104+
105+
_logger.LogInformation("Participant has been unblocked");
106+
return new BlockParticipantResult(true, "Participant Unblocked");
107+
108+
109+
}
110+
111+
public async Task<BlockParticipantResult> GetParticipant(BlockParticipantDto blockParticipantRequest)
112+
{
113+
114+
if (!ValidationHelper.ValidateNHSNumber(blockParticipantRequest.NhsNumber.ToString()))
115+
{
116+
_logger.LogWarning("Participant had an invalid NHS Number and cannot be blocked");
117+
return new BlockParticipantResult(false, "Invalid NHS Number");
118+
}
119+
120+
var participantDemographic = await _participantDemographicDataService.GetSingleByFilter(x => x.NhsNumber == blockParticipantRequest.NhsNumber);
121+
122+
if (participantDemographic != null)
123+
{
124+
var recordsMatch = ValidateRecordsMatch(participantDemographic, blockParticipantRequest);
125+
var responseBody = JsonSerializer.Serialize(new BlockParticipantDto
126+
{
127+
NhsNumber = participantDemographic.NhsNumber,
128+
FamilyName = participantDemographic.FamilyName!,
129+
DateOfBirth = participantDemographic.DateOfBirth!,
130+
});
131+
return new BlockParticipantResult(recordsMatch, responseBody);
132+
}
133+
134+
var pdsParticipant = await GetPDSParticipant(blockParticipantRequest.NhsNumber);
135+
136+
if (pdsParticipant == null)
137+
{
138+
return new BlockParticipantResult(false, "Participant Couldn't be found");
139+
}
140+
141+
var pdsRecordsMatch = ValidateRecordsMatch(pdsParticipant, blockParticipantRequest);
142+
var pdsResponseBody = JsonSerializer.Serialize(new BlockParticipantDto
143+
{
144+
NhsNumber = pdsParticipant.NhsNumber,
145+
FamilyName = pdsParticipant.FamilyName!,
146+
DateOfBirth = pdsParticipant.DateOfBirth!
147+
});
148+
149+
return new BlockParticipantResult(pdsRecordsMatch, pdsResponseBody);
150+
151+
}
152+
153+
private async Task<BlockParticipantResult> BlockNewParticipant(BlockParticipantDto blockParticipantRequest)
154+
{
155+
var pdsParticipant = await GetPDSParticipant(blockParticipantRequest.NhsNumber);
156+
157+
if (pdsParticipant == null || !ValidateRecordsMatch(pdsParticipant, blockParticipantRequest))
158+
{
159+
return new BlockParticipantResult(false, "Participant details do not match a records in Cohort Manager or PDS");
160+
}
161+
162+
var participantManagementRecord = new ParticipantManagement
163+
{
164+
NHSNumber = pdsParticipant.NhsNumber,
165+
BlockedFlag = 1,
166+
EligibilityFlag = 0,
167+
};
168+
169+
var participantManagementAdded = await _participantManagementDataService.Add(participantManagementRecord);
170+
171+
if (!participantManagementAdded)
172+
{
173+
return new BlockParticipantResult(false, "Unable to add participant to Cohort Manager to be blocked");
174+
}
175+
176+
return new BlockParticipantResult(true, "Participant Has been blocked");
177+
178+
179+
}
180+
181+
private async Task<BlockParticipantResult> BlockExistingParticipant(ParticipantManagement participant)
182+
{
183+
var blockFlagUpdated = await SetBlockedFlag(participant, true);
184+
185+
if (!blockFlagUpdated)
186+
{
187+
return new BlockParticipantResult(false, "Failed to Update participant in Cohort Manager");
188+
}
189+
190+
var unsubscribeFromNems = await UnsubscribeParticipantFromNEMS(participant.NHSNumber);
191+
192+
if (!unsubscribeFromNems)
193+
{
194+
return new BlockParticipantResult(false, "Failed to unsubscribe Participant From NEMS");
195+
}
196+
197+
return new BlockParticipantResult(true, "Participant Has been blocked");
198+
199+
}
200+
201+
private async Task<bool> UnsubscribeParticipantFromNEMS(long nhsNumber)
202+
{
203+
var nemsUnsubscribeResponse = await _httpClient.SendPost(_config.ManageNemsSubscriptionUnsubscribeURL, CreateNhsNumberQueryParams(nhsNumber));
204+
205+
return nemsUnsubscribeResponse.IsSuccessStatusCode;
206+
}
207+
208+
private async Task<bool> SubscribeParticipantToNEMS(long nhsNumber)
209+
{
210+
var nemsSubscribeResponse = await _httpClient.SendPost(_config.ManageNemsSubscriptionSubscribeURL, CreateNhsNumberQueryParams(nhsNumber));
211+
212+
return nemsSubscribeResponse.IsSuccessStatusCode;
213+
}
214+
215+
216+
private async Task<bool> SetBlockedFlag(ParticipantManagement participant, bool blocked)
217+
{
218+
participant.BlockedFlag = blocked ? (short)1 : (short)0;
219+
return await _participantManagementDataService.Update(participant);
220+
}
221+
222+
private async Task<ParticipantDemographic> GetPDSParticipant(long nhsNumber)
223+
{
224+
var pdsResponse = await _httpClient.SendGet(_config.RetrievePdsDemographicURL, CreateNhsNumberQueryParams(nhsNumber));
225+
if (string.IsNullOrEmpty(pdsResponse))
226+
{
227+
_logger.LogWarning("RetrievePDSDemographic Didn't return a valid response");
228+
return null!;
229+
}
230+
231+
var pdsDemographic = JsonSerializer.Deserialize<ParticipantDemographic>(pdsResponse);
232+
233+
return pdsDemographic!;
234+
}
235+
236+
private static bool ValidateRecordsMatch(ParticipantDemographic participant, BlockParticipantDto dto)
237+
{
238+
239+
if (!DateOnly.TryParseExact(dto.DateOfBirth, "yyyy-MM-dd",new CultureInfo("en-GB"),DateTimeStyles.None, out var dtoDateOfBirth ))
240+
{
241+
throw new FormatException("Date of Birth not in the correct format");
242+
}
243+
244+
if (!DateOnly.TryParseExact(participant.DateOfBirth, "yyyyMMdd",new CultureInfo("en-GB"),DateTimeStyles.None, out var parsedDob))
245+
{
246+
return false;
247+
}
248+
return string.Equals(participant.FamilyName, dto.FamilyName, StringComparison.InvariantCultureIgnoreCase)
249+
&& participant.NhsNumber == dto.NhsNumber
250+
&& parsedDob == dtoDateOfBirth;
251+
}
252+
253+
private static Dictionary<string, string> CreateNhsNumberQueryParams(long nhsNumber) =>
254+
new Dictionary<string, string>
255+
{
256+
{"nhsNumber",nhsNumber.ToString()}
257+
};
258+
259+
}

0 commit comments

Comments
 (0)