-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathManageServiceNowParticipantFunction.cs
More file actions
332 lines (273 loc) · 14.9 KB
/
ManageServiceNowParticipantFunction.cs
File metadata and controls
332 lines (273 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
namespace NHS.CohortManager.ParticipantManagementServices;
using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Common;
using DataServices.Client;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Model;
using Model.Constants;
using Model.Enums;
public class ManageServiceNowParticipantFunction
{
private readonly ILogger<ManageServiceNowParticipantFunction> _logger;
private readonly ManageServiceNowParticipantConfig _config;
private readonly IHttpClientFunction _httpClientFunction;
private readonly IExceptionHandler _exceptionHandler;
private readonly IDataServiceClient<ParticipantManagement> _participantManagementClient;
private readonly IQueueClient _queueClient;
private static readonly Regex NonLetterRegex = new(@"[^\p{Lu}\p{Ll}\p{Lt}]", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
public ManageServiceNowParticipantFunction(ILogger<ManageServiceNowParticipantFunction> logger, IOptions<ManageServiceNowParticipantConfig> config,
IHttpClientFunction httpClientFunction, IExceptionHandler handleException, IDataServiceClient<ParticipantManagement> participantManagementClient,
IQueueClient queueClient)
{
_logger = logger;
_config = config.Value;
_httpClientFunction = httpClientFunction;
_exceptionHandler = handleException;
_participantManagementClient = participantManagementClient;
_queueClient = queueClient;
}
/// <summary>
/// Reads messages from the Manage ServiceNow Participant Service Bus topic, checks PDS, adds/updates the record in participant management,
/// and sends the record to cohort distribution
/// </summary>
/// <param name="serviceNowParticipant">The participant from ServiceNow</param>
[Function(nameof(ManageServiceNowParticipantFunction))]
public async Task Run([ServiceBusTrigger(topicName: "%ServiceNowParticipantManagementTopic%", subscriptionName: "%ManageServiceNowParticipantSubscription%", Connection = "ServiceBusConnectionString_internal")] ServiceNowParticipant serviceNowParticipant)
{
try
{
var pdsDemographic = await ValidateAndRetrieveParticipantFromPds(serviceNowParticipant);
if (pdsDemographic is null)
{
return;
}
var participantManagement = await _participantManagementClient.GetSingleByFilter(
x => x.NHSNumber == serviceNowParticipant.NhsNumber && x.ScreeningId == serviceNowParticipant.ScreeningId);
var success = await ProcessParticipantRecord(serviceNowParticipant, participantManagement, pdsDemographic);
if (!success)
{
return;
}
var subscribeToNemsSuccess = await SubscribeParticipantToNEMS(serviceNowParticipant.NhsNumber);
if (!subscribeToNemsSuccess)
{
_logger.LogError("Failed to subscribe participant for updates. Case Number: {CaseNumber}", serviceNowParticipant.ServiceNowCaseNumber);
}
if (!string.IsNullOrEmpty(serviceNowParticipant.RequiredGpCode))
{
await _exceptionHandler.CreateTransformExecutedExceptions(new CohortDistributionParticipant { NhsNumber = serviceNowParticipant.NhsNumber.ToString() }, "98.UpdateServiceNowData.ReferralWithPrimaryCareProvider", 98);
}
var participantForDistribution = new BasicParticipantCsvRecord(serviceNowParticipant, participantManagement);
var sendToQueueSuccess = await _queueClient.AddAsync(participantForDistribution, _config.CohortDistributionTopic);
if (!sendToQueueSuccess)
{
await HandleException(new Exception($"Failed to send participant from ServiceNow to topic: {_config.CohortDistributionTopic}"), serviceNowParticipant, ServiceNowMessageType.AddRequestInProgress);
}
}
catch (Exception ex)
{
await HandleException(ex, serviceNowParticipant, ServiceNowMessageType.AddRequestInProgress);
}
}
private async Task<PdsDemographic?> ValidateAndRetrieveParticipantFromPds(ServiceNowParticipant serviceNowParticipant)
{
var pdsResponse = await _httpClientFunction.SendGetResponse($"{_config.RetrievePdsDemographicURL}?nhsNumber={serviceNowParticipant.NhsNumber}");
if (pdsResponse.StatusCode == HttpStatusCode.NotFound)
{
await HandleException(new Exception("Request to PDS for ServiceNow Participant returned a NotFound response."), serviceNowParticipant, ServiceNowMessageType.UnableToAddParticipant);
return null;
}
if (pdsResponse.StatusCode != HttpStatusCode.OK)
{
await HandleException(new Exception($"Request to PDS for ServiceNow Participant returned an unexpected response. Status code: {pdsResponse.StatusCode}"), serviceNowParticipant, ServiceNowMessageType.AddRequestInProgress);
return null;
}
var pdsDemographic = await DeserializePdsDemographic(pdsResponse, serviceNowParticipant);
if (pdsDemographic is null) return null;
return await ValidateParticipantData(serviceNowParticipant, pdsDemographic)
? pdsDemographic
: null;
}
private async Task<PdsDemographic?> DeserializePdsDemographic(HttpResponseMessage pdsResponse, ServiceNowParticipant serviceNowParticipant)
{
var pdsDemographic = await pdsResponse.Content.ReadFromJsonAsync<PdsDemographic>();
if (pdsDemographic is null)
{
await HandleException(new Exception($"Deserialisation of PDS for ServiceNow Participant response to {typeof(PdsDemographic)} returned null"), serviceNowParticipant, ServiceNowMessageType.AddRequestInProgress);
return null;
}
return pdsDemographic;
}
private async Task<bool> ValidateParticipantData(ServiceNowParticipant serviceNowParticipant, PdsDemographic pdsDemographic)
{
if (pdsDemographic.NhsNumber != serviceNowParticipant.NhsNumber.ToString())
{
await HandleException(new Exception("NHS Numbers don't match for ServiceNow Participant and PDS, NHS Number must have been superseded"), serviceNowParticipant, ServiceNowMessageType.UnableToAddParticipant);
return false;
}
if (!CheckParticipantDataMatches(serviceNowParticipant, pdsDemographic))
{
await HandleException(new Exception("Participant data from ServiceNow does not match participant data from PDS"), serviceNowParticipant, ServiceNowMessageType.UnableToAddParticipant);
return false;
}
return true;
}
private async Task<bool> ProcessParticipantRecord(ServiceNowParticipant serviceNowParticipant, ParticipantManagement? participantManagement, PdsDemographic pdsDemographic)
{
var success = false;
string? failureDescription;
if (participantManagement is null)
{
success = await AddNewParticipant(serviceNowParticipant, pdsDemographic);
failureDescription = "Participant Management Data Service add request failed";
}
else if (participantManagement.BlockedFlag == 1)
{
failureDescription = "Participant data from ServiceNow is blocked";
}
else
{
success = await UpdateExistingParticipant(serviceNowParticipant, participantManagement, pdsDemographic);
failureDescription = "Participant Management Data Service update request failed";
}
if (!success)
{
await HandleException(new Exception(failureDescription), serviceNowParticipant, ServiceNowMessageType.UnableToAddParticipant);
}
return success;
}
private async Task<bool> AddNewParticipant(ServiceNowParticipant serviceNowParticipant, PdsDemographic pdsDemographic)
{
_logger.LogInformation("Participant not in participant management table, adding new record");
var isVhrParticipant = CheckIfVhrParticipant(serviceNowParticipant);
var participantToAdd = new ParticipantManagement
{
ScreeningId = serviceNowParticipant.ScreeningId,
NHSNumber = serviceNowParticipant.NhsNumber,
RecordType = Actions.New,
EligibilityFlag = 1,
ReferralFlag = 1,
RecordInsertDateTime = DateTime.UtcNow,
IsHigherRisk = isVhrParticipant ? 1 : null,
ReasonForRemoval = pdsDemographic.ReasonForRemoval,
ReasonForRemovalDate = ParseRemovalEffectiveFromDateStringToDateTime(pdsDemographic.RemovalEffectiveFromDate)
};
if (isVhrParticipant)
{
_logger.LogInformation("Participant set as High Risk");
}
return await _participantManagementClient.Add(participantToAdd);
}
private async Task<bool> UpdateExistingParticipant(ServiceNowParticipant serviceNowParticipant, ParticipantManagement participantManagement, PdsDemographic pdsDemographic)
{
_logger.LogInformation("Existing participant management record found, updating record {ParticipantId}", participantManagement.ParticipantId);
participantManagement.RecordType = Actions.New;
participantManagement.EligibilityFlag = 1;
participantManagement.ReferralFlag = 1;
participantManagement.ExceptionFlag = 0;
participantManagement.RecordUpdateDateTime = DateTime.UtcNow;
participantManagement.ReasonForRemoval = pdsDemographic.ReasonForRemoval;
participantManagement.ReasonForRemovalDate = ParseRemovalEffectiveFromDateStringToDateTime(pdsDemographic.RemovalEffectiveFromDate);
HandleVhrFlagForExistingParticipant(serviceNowParticipant, participantManagement);
return await _participantManagementClient.Update(participantManagement);
}
private static DateTime? ParseRemovalEffectiveFromDateStringToDateTime(string? removalEffectiveFromDate)
{
if (removalEffectiveFromDate == null)
{
return null;
}
return DateTime.ParseExact(removalEffectiveFromDate, "yyyy'-'MM'-'dd'T'HH':'mm':'ssK", CultureInfo.InvariantCulture);
}
private void HandleVhrFlagForExistingParticipant(ServiceNowParticipant serviceNowParticipant, ParticipantManagement participantManagement)
{
var isVhrParticipant = CheckIfVhrParticipant(serviceNowParticipant);
if (!participantManagement.IsHigherRisk.HasValue && isVhrParticipant)
{
participantManagement.IsHigherRisk = 1;
_logger.LogInformation("Participant {ParticipantId} set as High Risk based on ServiceNow attributes", participantManagement.ParticipantId);
}
if (participantManagement.IsHigherRisk == 1)
{
_logger.LogInformation("Participant {ParticipantId} still maintained as High Risk", participantManagement.ParticipantId);
}
}
private async Task HandleException(Exception exception, ServiceNowParticipant serviceNowParticipant, ServiceNowMessageType serviceNowMessageType)
{
_logger.LogError(exception, "Exception occurred whilst attempting to add participant from ServiceNow");
await _exceptionHandler.CreateSystemExceptionLog(exception, serviceNowParticipant);
await SendServiceNowMessage(serviceNowParticipant.ServiceNowCaseNumber, serviceNowMessageType);
}
private static bool CheckParticipantDataMatches(ServiceNowParticipant serviceNowParticipant, PdsDemographic pdsDemographic)
{
return NormalizedNamesMatch(serviceNowParticipant.FirstName, pdsDemographic.FirstName) &&
NormalizedNamesMatch(serviceNowParticipant.FamilyName, pdsDemographic.FamilyName) &&
serviceNowParticipant.DateOfBirth.ToString("yyyy-MM-dd") == pdsDemographic.DateOfBirth;
}
/// <summary>
/// Normalizes and compares two name strings by removing accents, spaces, hyphens, and special characters.
/// Converts accented characters to their base forms (É→E, Ñ→N, Ö→O) to match database storage behavior.
/// </summary>
/// <param name="name1">First name to compare</param>
/// <param name="name2">Second name to compare</param>
/// <returns>True if the normalized names match (case-insensitive), false otherwise</returns>
private static bool NormalizedNamesMatch(string? name1, string? name2)
{
if (string.IsNullOrWhiteSpace(name1) && string.IsNullOrWhiteSpace(name2)) return true;
if (string.IsNullOrWhiteSpace(name1) || string.IsNullOrWhiteSpace(name2)) return false;
var normalized1 = NormalizeName(name1);
var normalized2 = NormalizeName(name2);
if (string.IsNullOrEmpty(normalized1) || string.IsNullOrEmpty(normalized2)) return false;
return string.Equals(normalized1, normalized2, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Normalizes a name by removing accents and all non-letter characters.
/// This handles spaces, hyphens, apostrophes, and other punctuation.
/// Accented characters like É, Ñ, Ö are converted to their base forms (E, N, O).
/// Uses Unicode NFD normalization to decompose accents, then removes diacritical marks.
/// </summary>
/// <param name="name">The name to normalize</param>
/// <returns>Normalized name containing only unaccented ASCII letters</returns>
private static string NormalizeName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return string.Empty;
}
var trimmedName = name.Trim();
var normalizedString = trimmedName.Normalize(NormalizationForm.FormD);
var lettersOnlyString = NonLetterRegex.Replace(normalizedString, string.Empty);
return lettersOnlyString.Normalize(NormalizationForm.FormC);
}
private async Task SendServiceNowMessage(string serviceNowCaseNumber, ServiceNowMessageType servicenowMessageType)
{
var url = $"{_config.SendServiceNowMessageURL}/{serviceNowCaseNumber}";
var requestBody = new SendServiceNowMessageRequestBody
{
MessageType = servicenowMessageType
};
var json = JsonSerializer.Serialize(requestBody);
_logger.LogInformation("Sending ServiceNow message type {MessageType}", servicenowMessageType);
await _httpClientFunction.SendPut(url, json);
}
private static bool CheckIfVhrParticipant(ServiceNowParticipant serviceNowParticipant)
{
return serviceNowParticipant.ReasonForAdding == ServiceNowReasonsForAdding.VeryHighRisk;
}
private async Task<bool> SubscribeParticipantToNEMS(long nhsNumber)
{
var queryParams = new Dictionary<string, string>
{
{"nhsNumber", nhsNumber.ToString()}
};
var nemsSubscribeResponse = await _httpClientFunction.SendPost(_config.ManageNemsSubscriptionSubscribeURL, queryParams);
return nemsSubscribeResponse.IsSuccessStatusCode;
}
}