Skip to content

Commit 7e9aa12

Browse files
Merge branch 'main' into feat/nems-replacement-mesh-branch
2 parents a2971cd + c858273 commit 7e9aa12

33 files changed

Lines changed: 1276 additions & 549 deletions

File tree

application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,6 @@
2626
}
2727
}
2828
},
29-
{
30-
"RuleName": "62.ValidateReasonForRemoval.NBO.NonFatal",
31-
"Expression": "!(participant.ReasonForRemoval == \"LDN\" AND string.IsNullOrEmpty(participant.SupersededByNhsNumber))",
32-
"Actions": {
33-
"OnFailure": {
34-
"Name": "OutputExpression",
35-
"Context": {
36-
"Expression": "\"Reason for removal and superseded NHS ID values incompatible\""
37-
}
38-
}
39-
}
40-
},
4129
{
4230
"RuleName": "53.CurrentPostingAndPrimaryCareProvider.NBO.NonFatal",
4331
"LocalParams": [
@@ -59,36 +47,6 @@
5947
}
6048
}
6149
}
62-
},
63-
{
64-
"RuleName": "94.EligibilityFlag.CaaS.NonFatal",
65-
"LocalParams": [
66-
{
67-
"Name": "newRecordType",
68-
"Expression": "participant.RecordType == Actions.New"
69-
},
70-
{
71-
"Name": "validNewEligibilityFlag",
72-
"Expression": "participant.EligibilityFlag != \"0\""
73-
},
74-
{
75-
"Name": "amendRecordType",
76-
"Expression": "participant.RecordType == Actions.Amended"
77-
},
78-
{
79-
"Name": "validAmendEligibilityFlag",
80-
"Expression": "(participant.EligibilityFlag == \"1\") OR (participant.EligibilityFlag == \"0\")"
81-
}
82-
],
83-
"Expression": "(newRecordType AND validNewEligibilityFlag) OR (amendRecordType AND validAmendEligibilityFlag)",
84-
"Actions": {
85-
"OnFailure": {
86-
"Name": "OutputExpression",
87-
"Context": {
88-
"Expression": "\"Invalid eligibility flag.\""
89-
}
90-
}
91-
}
9250
}
9351
]
9452
},
@@ -216,22 +174,5 @@
216174
}
217175
}
218176
]
219-
},
220-
{
221-
"WorkflowName": "DEL",
222-
"Rules": [
223-
{
224-
"RuleName": "94.EligibilityFlag.CaaS.NonFatal",
225-
"Expression": "participant.EligibilityFlag == \"0\"",
226-
"Actions": {
227-
"OnFailure": {
228-
"Name": "OutputExpression",
229-
"Context": {
230-
"Expression": "\"Invalid eligibility flag.\""
231-
}
232-
}
233-
}
234-
}
235-
]
236177
}
237178
]

application/CohortManager/src/Functions/Shared/Common/HttpParserHelper.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Common;
22

3+
using System.Globalization;
34
using System.Net;
45
using Common.Interfaces;
56
using Microsoft.Azure.Functions.Worker.Http;
@@ -69,4 +70,45 @@ public static T GetEnumQueryParameter<T>(HttpRequestData req, string key, T defa
6970

7071
return Enum.TryParse<T>(queryString, true, out var result) ? result : defaultValue;
7172
}
73+
74+
/// <summary>
75+
/// Parses a DateTime query parameter from the request. Supports various date formats.
76+
/// </summary>
77+
/// <param name="req">The HTTP request data</param>
78+
/// <param name="key">The query parameter key name</param>
79+
/// <returns>The parsed DateTime value or null if parsing fails or parameter is missing</returns>
80+
public DateTime? GetQueryParameterAsDateTime(HttpRequestData req, string key)
81+
{
82+
var queryString = req.Query[key];
83+
84+
if (string.IsNullOrWhiteSpace(queryString))
85+
{
86+
return null;
87+
}
88+
89+
string[] formats = {
90+
"yyyy-MM-dd",
91+
"yyyy/MM/dd",
92+
"dd/MM/yyyy",
93+
"dd-MM-yyyy",
94+
"MM/dd/yyyy",
95+
"MM-dd-yyyy",
96+
"yyyy-MM-ddTHH:mm:ss",
97+
"yyyy-MM-ddTHH:mm:ssZ",
98+
"yyyy-MM-dd HH:mm:ss"
99+
};
100+
101+
if (DateTime.TryParse(queryString, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result))
102+
{
103+
return result;
104+
}
105+
106+
if (DateTime.TryParseExact(queryString, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
107+
{
108+
return result;
109+
}
110+
111+
_logger.LogWarning("Failed to parse date parameter '{Key}' with value '{Value}'", key, queryString);
112+
return null;
113+
}
72114
}

application/CohortManager/src/Functions/Shared/Common/Interfaces/IHttpParserHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public interface IHttpParserHelper
77
HttpResponseData LogErrorResponse(HttpRequestData req, string errorMessage);
88
int GetQueryParameterAsInt(HttpRequestData req, string key, int defaultValue = 0);
99
bool GetQueryParameterAsBool(HttpRequestData req, string key, bool defaultValue = false);
10+
DateTime? GetQueryParameterAsDateTime(HttpRequestData req, string key);
1011
};

application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ namespace Common;
55
public interface IPaginationService<T>
66
{
77
PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int? lastId, Func<T, int>? idSelector = null);
8-
Dictionary<string, string> BuildPaginationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult);
8+
Dictionary<string, string> AddNavigationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult);
99
}

application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int? lastId,
2929
}
3030

3131
var items = source.Take(pageSize).ToList();
32-
int? lastResultId = items.Count > 0 ? idSelector(items[items.Count - 1]) : null;
32+
int? lastResultId = items.Count > 0 ? idSelector(items[^1]) : null;
3333

3434
return new PaginationResult<T>
3535
{
@@ -39,11 +39,14 @@ public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int? lastId,
3939
LastResultId = lastResultId,
4040
TotalItems = totalItems,
4141
TotalPages = totalPages,
42-
CurrentPage = currentPage
42+
CurrentPage = currentPage,
4343
};
4444
}
4545

46-
public Dictionary<string, string> BuildPaginationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult)
46+
/// <summary>
47+
/// Adds pagination navigation headers to the response.
48+
/// </summary>
49+
public Dictionary<string, string> AddNavigationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult)
4750
{
4851
var headers = new Dictionary<string, string>
4952
{
@@ -100,15 +103,16 @@ private static string RemoveLastIdParam(string queryString)
100103

101104
private static Func<T, int> GetDefaultIdSelector()
102105
{
103-
var idProperty = typeof(T).GetProperty("Id") ??
104-
typeof(T).GetProperty($"{typeof(T).Name}Id");
105-
106-
if (idProperty == null)
107-
{
106+
var idProperty = (typeof(T).GetProperty("Id") ??
107+
typeof(T).GetProperty($"{typeof(T).Name}Id")) ??
108108
throw new InvalidOperationException(
109109
"Could not find a default ID property. Provide a custom ID selector.");
110-
}
111110

112-
return x => (int)idProperty.GetValue(x);
111+
return x =>
112+
{
113+
var value = idProperty.GetValue(x) ?? throw new InvalidOperationException(
114+
$"Entity of type {typeof(T).Name} has a null ID property ('{idProperty.Name}').");
115+
return (int)value;
116+
};
113117
}
114118
}

application/CohortManager/src/Functions/Shared/Data/Database/IValidationExceptionData.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ public interface IValidationExceptionData
1111
Task<ValidationException?> GetExceptionById(int exceptionId);
1212
Task<bool> RemoveOldException(string nhsNumber, string screeningName);
1313
Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptionId, string serviceNowId);
14+
Task<List<ValidationException>?> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory);
1415
}

application/CohortManager/src/Functions/Shared/Data/Database/ValidationExceptionData.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,42 @@ public async Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptio
120120
}
121121
}
122122

123+
public async Task<List<ValidationException>?> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory)
124+
{
125+
if (exceptionCategory is not (ExceptionCategory.Confusion or ExceptionCategory.Superseded or ExceptionCategory.NBO))
126+
{
127+
return [];
128+
}
129+
130+
var filteredExceptions = (await _validationExceptionDataServiceClient.GetByFilter(x =>
131+
x.Category.HasValue && (x.Category.Value == (int)ExceptionCategory.Confusion || x.Category.Value == (int)ExceptionCategory.Superseded)))?.AsEnumerable();
132+
133+
if (exceptionCategory == ExceptionCategory.Confusion || exceptionCategory == ExceptionCategory.Superseded)
134+
{
135+
filteredExceptions = filteredExceptions?.Where(x => x.Category.HasValue && x.Category.Value == (int)exceptionCategory);
136+
}
137+
138+
if (reportDate.HasValue)
139+
{
140+
var startDate = reportDate.Value.Date;
141+
var endDate = startDate.AddDays(1);
142+
filteredExceptions = filteredExceptions?.Where(x => x.DateCreated >= startDate && x.DateCreated < endDate);
143+
}
144+
145+
if (filteredExceptions?.Any() != true)
146+
return [];
147+
148+
var tasks = filteredExceptions.Select(async exception =>
149+
{
150+
var validationException = exception.ToValidationException();
151+
var participantDemographic = long.TryParse(exception.NhsNumber, out long nhsNumber) ? await _demographicDataServiceClient.GetSingleByFilter(x => x.NhsNumber == nhsNumber) : null;
152+
return GetExceptionDetails(validationException, participantDemographic);
153+
});
154+
155+
var results = await Task.WhenAll(tasks);
156+
return results.Where(x => x != null).ToList()!;
157+
}
158+
123159
private ServiceResponseModel CreateResponse(bool success, HttpStatusCode statusCode, string message)
124160
{
125161
if (!success)
@@ -187,7 +223,7 @@ private ServiceResponseModel CreateResponse(bool success, HttpStatusCode statusC
187223
{
188224

189225
var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.NhsNumber == nhsNumber && x.ScreeningName == screeningName);
190-
return exceptions != null ? exceptions.ToList() : null;
226+
return exceptions?.ToList();
191227

192228
}
193229

application/CohortManager/src/Functions/Shared/Model/Enums/ExceptionCategory.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ public enum ExceptionCategory
99
File = 5,
1010
NilReturnFile = 7,
1111
DeleteRecord = 8,
12-
ParticipantLocationRemainingOutsideOfCohort = 9,
1312
Schema = 10,
1413
TransformExecuted = 11,
1514
Confusion = 12,

application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,9 @@ namespace NHS.CohortManager.ScreeningDataServices;
1313
using Model.Enums;
1414

1515
/// <summary>
16-
/// Azure Function for retrieving cohort distribution data based on ScreeningServiceId.
16+
/// Azure Function for retrieving and managing validation exceptions.
17+
/// Provides endpoints for exception queries, reports, and ServiceNowId updates.
1718
/// </summary>
18-
/// <param name="req">The HTTP request data containing query parameters and request details.</param>
19-
/// <param name="exceptionId">query parameter used to search for an exception by Id..</param>
20-
/// if not exceptionId is passed in the full list of exceptions will be returned
21-
/// <returns>
22-
/// HTTP response with:
23-
/// - 204 No Content if no data is found.
24-
/// - 200 OK - List<GetValidationExceptions> or single GetValidationExcept in JSON format .
25-
/// - 500 Internal Server Error if an exception occurs.
26-
/// </returns>
2719
public class GetValidationExceptions
2820
{
2921
private readonly ILogger<GetValidationExceptions> _logger;
@@ -32,7 +24,6 @@ public class GetValidationExceptions
3224
private readonly IHttpParserHelper _httpParserHelper;
3325
private readonly IPaginationService<ValidationException> _paginationService;
3426

35-
3627
public GetValidationExceptions(ILogger<GetValidationExceptions> logger, ICreateResponse createResponse, IValidationExceptionData validationData, IHttpParserHelper httpParserHelper, IPaginationService<ValidationException> paginationService)
3728
{
3829
_logger = logger;
@@ -42,6 +33,15 @@ public GetValidationExceptions(ILogger<GetValidationExceptions> logger, ICreateR
4233
_paginationService = paginationService;
4334
}
4435

36+
/// <summary>
37+
/// Retrieves validation exceptions based on query parameters.
38+
/// Supports single exception lookup, filtered lists, and report-based queries.
39+
/// </summary>
40+
/// <param name="req">The HTTP request data containing query parameters.</param>
41+
/// <returns>
42+
/// HTTP response containing validation exceptions in JSON format.
43+
/// Returns 200 OK with data, 204 No Content if empty, 400 Bad Request for validation errors, or 500 Internal Server Error.
44+
/// </returns>
4545
[Function(nameof(GetValidationExceptions))]
4646
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
4747
{
@@ -50,33 +50,32 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
5050
var exceptionStatus = HttpParserHelper.GetEnumQueryParameter(req, "exceptionStatus", ExceptionStatus.All);
5151
var sortOrder = HttpParserHelper.GetEnumQueryParameter(req, "sortOrder", SortOrder.Descending);
5252
var exceptionCategory = HttpParserHelper.GetEnumQueryParameter(req, "exceptionCategory", ExceptionCategory.NBO);
53+
var reportDate = _httpParserHelper.GetQueryParameterAsDateTime(req, "reportDate");
54+
var isReport = _httpParserHelper.GetQueryParameterAsBool(req, "isReport");
5355

5456
try
5557
{
5658
if (exceptionId > 0)
5759
{
5860
var exceptionById = await _validationData.GetExceptionById(exceptionId);
5961
return exceptionById == null
60-
? _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req)
61-
: _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, JsonSerializer.Serialize(exceptionById));
62+
? _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req)
63+
: _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, JsonSerializer.Serialize(exceptionById));
6264
}
6365

64-
var allExceptions = await _validationData.GetAllFilteredExceptions(exceptionStatus, sortOrder, exceptionCategory);
65-
66-
if (allExceptions == null)
66+
if (isReport)
6767
{
68-
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
69-
}
68+
if (reportDate.HasValue && reportDate.Value > DateTime.Now.Date)
69+
{
70+
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req, "Report date cannot be in the future.");
71+
}
7072

71-
var paginatedResult = _paginationService.GetPaginatedResult(allExceptions.AsQueryable(), lastId == 0 ? null : lastId, e => e.ExceptionId);
72-
if (!paginatedResult.Items.Any())
73-
{
74-
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
73+
var reportExceptions = await _validationData.GetReportExceptions(reportDate, exceptionCategory);
74+
return CreatePaginatedResponse(req, reportExceptions, lastId);
7575
}
7676

77-
var headers = _paginationService.BuildPaginationHeaders(req, paginatedResult);
78-
79-
return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, req, JsonSerializer.Serialize(paginatedResult.Items), headers);
77+
var allExceptions = await _validationData.GetAllFilteredExceptions(exceptionStatus, sortOrder, exceptionCategory);
78+
return CreatePaginatedResponse(req, allExceptions, lastId);
8079
}
8180
catch (Exception ex)
8281
{
@@ -85,16 +84,28 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
8584
}
8685
}
8786

87+
88+
private HttpResponseData CreatePaginatedResponse(HttpRequestData req, List<ValidationException>? exceptions, int lastId)
89+
{
90+
if (exceptions == null || exceptions.Count == 0)
91+
{
92+
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
93+
}
94+
95+
var paginatedResult = _paginationService.GetPaginatedResult(exceptions.AsQueryable(), lastId == 0 ? null : lastId, e => e.ExceptionId);
96+
97+
if (!paginatedResult.Items.Any())
98+
{
99+
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
100+
}
101+
102+
var headers = _paginationService.AddNavigationHeaders(req, paginatedResult);
103+
return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, req, JsonSerializer.Serialize(paginatedResult), headers);
104+
}
105+
88106
/// <summary>
89-
/// Updates the ServiceNow ID for a specific validation exception.
107+
/// Updates the ServiceNowId for a specific validation exception.
90108
/// </summary>
91-
/// <param name="req">The HTTP request data containing the exception ID and ServiceNow ID.</param>
92-
/// <returns>
93-
/// HTTP response with:
94-
/// - 200 OK if the update is successful
95-
/// - 400 Bad Request if required parameters are missing
96-
/// - 500 Internal Server Error if an exception occurs
97-
/// </returns>
98109
[Function(nameof(UpdateExceptionServiceNowId))]
99110
public async Task<HttpResponseData> UpdateExceptionServiceNowId([HttpTrigger(AuthorizationLevel.Anonymous, "put")] HttpRequestData req)
100111
{

0 commit comments

Comments
 (0)