Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
653f9ad
feat: Add PDS Stub
Joseph2910 Jul 23, 2025
d3e24bf
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Jul 30, 2025
c960229
block participant refactor
MWClayson-NHS Jul 31, 2025
2c82910
Stubbing Nems API
MWClayson-NHS Aug 4, 2025
fc9c802
Update Logging and returning body
MWClayson-NHS Aug 4, 2025
b067b34
get participant and bug fixes
MWClayson-NHS Aug 5, 2025
00a155c
unblock participant
MWClayson-NHS Aug 5, 2025
cd08964
tidying
MWClayson-NHS Aug 5, 2025
897f940
Eligbility flag logic
MWClayson-NHS Aug 5, 2025
f68f937
Logging and remove rule
MWClayson-NHS Aug 6, 2025
45c1f94
intial tests
MWClayson-NHS Aug 6, 2025
b4b2277
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 6, 2025
b1f4114
tfvars and compose
MWClayson-NHS Aug 6, 2025
22c8b84
function signature overlap
MWClayson-NHS Aug 6, 2025
6dba115
some sonar qube fixes
MWClayson-NHS Aug 6, 2025
a6f0328
more sonar qube fixes
MWClayson-NHS Aug 6, 2025
1536ba8
more sonar qube fixes
MWClayson-NHS Aug 6, 2025
52ee103
more sonar qube
MWClayson-NHS Aug 6, 2025
05b2d87
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 6, 2025
bdd0ddd
tests part one
MWClayson-NHS Aug 6, 2025
b8cb0f1
Correct Logging from PR Comment
MWClayson-NHS Aug 6, 2025
2379d50
Addressing comments
MWClayson-NHS Aug 6, 2025
d32e4df
Incorrect Logic comment
MWClayson-NHS Aug 6, 2025
973ed23
Block Participant Tests
MWClayson-NHS Aug 6, 2025
7e79391
Addressing Comments
MWClayson-NHS Aug 6, 2025
d22b07d
Sonar Qube
MWClayson-NHS Aug 6, 2025
801ea55
Tests
MWClayson-NHS Aug 7, 2025
6ace84b
remove duplicate test
MWClayson-NHS Aug 7, 2025
1c09833
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 7, 2025
0f80b54
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 7, 2025
1078949
Addressing Comments
MWClayson-NHS Aug 7, 2025
f53c8ef
using const for cannot parse message
MWClayson-NHS Aug 7, 2025
227b73c
unused usings
MWClayson-NHS Aug 7, 2025
cc85893
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 7, 2025
efaf6b6
Merge branch 'main' into Block-Phase-2-PDS
MWClayson-NHS Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions application/CohortManager/compose.core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ services:
- ParticipantManagementUrl=http://participant-management-data-service:7994/api/ParticipantManagementDataService
- ParticipantDemographicDataServiceURL=http://participant-demographic-data-service:7993/api/ParticipantDemographicDataService/
- ExceptionFunctionURL=http://create-exception:7070/api/CreateException
- ManageNemsSubscriptionUnsubscribeURL=http://manage-nems-subscription:9081/api/Unsubscribe
- ManageNemsSubscriptionSubscribeURL=http://manage-nems-subscription:9081/api/ManageNemsSubscriptionSubscribeURL
- RetrievePdsDemographicURL=http://etrieve-pds-demographic:8082/api/RetrievePdsDemographic

delete-participant:
container_name: delete-participant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public class ReceiveCaasFileConfig
public string caasfolder_STORAGE { get; set; }
[Required]
public string inboundBlobName { get; set; }
[Required]
public string ServiceBusConnectionString_client_internal { get; set; }
public string GetOrchestrationStatusURL { get; set; }
[Required]
public string ParticipantManagementTopic { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
namespace NHS.CohortManager.DemographicServices;

using System.Security.Cryptography.X509Certificates;
using Azure.Identity;
using Azure.Security.KeyVault.Certificates;
using Microsoft.Extensions.Logging;

namespace NHS.CohortManager.DemographicServices;


public static class CertificateExtensions
{
Expand All @@ -26,15 +28,16 @@ public static async Task<X509Certificate2> LoadNemsCertificateAsync(this ManageN
var certResult = await certClient.DownloadCertificateAsync(config.NemsKeyName);
return certResult.Value;
}

if (!string.IsNullOrEmpty(config.NemsLocalCertPath))
{
logger.LogInformation("Loading NEMS certificate from local file");
return !string.IsNullOrEmpty(config.NemsLocalCertPassword)
? new X509Certificate2(config.NemsLocalCertPath, config.NemsLocalCertPassword)
: new X509Certificate2(config.NemsLocalCertPath);

}

throw new InvalidOperationException("No certificate configuration found. Please configure either KeyVaultConnectionString or NemsLocalCertPath.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,6 @@ public class ManageNemsSubscriptionConfig
/// </summary>
public bool NemsBypassServerCertificateValidation { get; set; } = false;

/// <summary>
/// Default event types to subscribe to
/// </summary>
public string[] NemsDefaultEventTypes { get; set; } = new[]
{
"pds-record-change-1"
};

/// <summary>
/// HTTP client timeout in seconds for NEMS API requests
/// Default: 300 seconds (5 minutes)
Expand All @@ -101,5 +93,9 @@ public class ManageNemsSubscriptionConfig
/// Custom validation to ensure either KeyVault or local cert is configured
/// </summary>
public bool IsValid => !string.IsNullOrEmpty(KeyVaultConnectionString) || !string.IsNullOrEmpty(NemsLocalCertPath);
/// <summary>
/// Bool to set the function to be in stubbed mode. Simulated responses from NEMS
/// </summary>
public bool IsStubbed { get; set; } = false;
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,37 @@
using NHS.CohortManager.DemographicServices;
using DataServices.Database;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography.X509Certificates;

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

var host = new HostBuilder();


logger.LogInformation("Application Has Started");
// Load configuration
host.AddConfiguration<ManageNemsSubscriptionConfig>(out ManageNemsSubscriptionConfig config);

var nemsConfig = config;

X509Certificate2 nemsCertificate;


// Load NEMS certificate up-front and inject into DI
var nemsCertificate = await nemsConfig.LoadNemsCertificateAsync(logger);

nemsCertificate = await nemsConfig.LoadNemsCertificateAsync(logger);


host.ConfigureFunctionsWebApplication();
host.AddHttpClient()
.AddNemsHttpClient()
.AddNemsHttpClient(nemsConfig.IsStubbed)
.ConfigureServices(services =>
{
// Register NEMS certificate
services.AddSingleton(nemsCertificate);


// Register NEMS subscription manager
services.AddScoped<NemsSubscriptionManager>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NHS.CohortManager.ParticipantManagementService;

public class BlockParticipantDto
{
public required long NhsNumber { get; set; }
public required string DateOfBirth { get; set; }
public required string FamilyName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
namespace NHS.CohortManager.ParticipantManagementService;

using System;
using System.Globalization;
using System.Text.Json;
using Common;
using DataServices.Client;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Model;

public class BlockParticipantHandler : IBlockParticipantHandler
{
private readonly ILogger<BlockParticipantHandler> _logger;
private readonly IDataServiceClient<ParticipantManagement> _participantManagementDataService;
private readonly IDataServiceClient<ParticipantDemographic> _participantDemographicDataService;
private readonly IHttpClientFunction _httpClient;
private readonly UpdateBlockedFlagConfig _config;
public BlockParticipantHandler(ILogger<BlockParticipantHandler> logger,
IDataServiceClient<ParticipantManagement> participantManagementDataService,
IDataServiceClient<ParticipantDemographic> participantDemographicDataService,
IHttpClientFunction httpClient,
IOptions<UpdateBlockedFlagConfig> config
)
{
_logger = logger;
_participantManagementDataService = participantManagementDataService;
_participantDemographicDataService = participantDemographicDataService;
_httpClient = httpClient;
_config = config.Value;
}

public async Task<BlockParticipantResult> BlockParticipant(BlockParticipantDto blockParticipantRequest)
{
if (!ValidationHelper.ValidateNHSNumber(blockParticipantRequest.NhsNumber.ToString()))
{
_logger.LogWarning("Participant had an invalid NHS Number and cannot be blocked");
return new BlockParticipantResult(false, "Invalid NHS Number");
}



var participantManagementRecord = await _participantManagementDataService.GetSingleByFilter(x => x.NHSNumber == blockParticipantRequest.NhsNumber);

if (participantManagementRecord == null)
{
return await BlockNewParticipant(blockParticipantRequest);
}

if (participantManagementRecord.BlockedFlag == 1)
{
_logger.LogWarning("Participant already blocked and cannot be blocked");
return new BlockParticipantResult(false, "Participant Already Blocked");
}

var participantDemographic = await _participantDemographicDataService.GetSingleByFilter(x => x.NhsNumber == blockParticipantRequest.NhsNumber);

if (!ValidateRecordsMatch(participantDemographic, blockParticipantRequest))
{
_logger.LogWarning("Participant didn't pass three point check and cannot be blocked");
return new BlockParticipantResult(false, "Participant Didn't pass three point check");
}

_logger.LogInformation("Participant has been blocked");
return await BlockExistingParticipant(participantManagementRecord);



}

public async Task<BlockParticipantResult> UnblockParticipant(long nhsNumber)
{

var participantManagementRecord = await _participantManagementDataService.GetSingleByFilter(x => x.NHSNumber == nhsNumber);

if (participantManagementRecord == null)
{
return new BlockParticipantResult(false, "Participant Couldn't be found");
}

if (participantManagementRecord.BlockedFlag != 1)
Comment thread
SamAinsworth-NHS marked this conversation as resolved.
{
_logger.LogInformation("Participant couldn't be unblocked as they are not currently blocked");
return new BlockParticipantResult(false, "Participant is not blocked");
}


var blockedFlagSet = await SetBlockedFlag(participantManagementRecord, false);
if (!blockedFlagSet)
{
return new BlockParticipantResult(false, "Failed to unset blocked flag");
}

if (participantManagementRecord.EligibilityFlag == 0)
{
return new BlockParticipantResult(true, "Participant was unblocked but not resubscribed to Nems as they are ineligible");
Comment thread
alex-clayton-1 marked this conversation as resolved.
}

var nemsSubscribed = await SubscribeParticipantToNEMS(nhsNumber);
if (!nemsSubscribed)
{
return new BlockParticipantResult(false, "Participant couldn't be subscribed in Nems");
}

_logger.LogInformation("Participant has been unblocked");
return new BlockParticipantResult(true, "Participant Unblocked");


}

public async Task<BlockParticipantResult> GetParticipant(BlockParticipantDto blockParticipantRequest)
{

if (!ValidationHelper.ValidateNHSNumber(blockParticipantRequest.NhsNumber.ToString()))
{
_logger.LogWarning("Participant had an invalid NHS Number and cannot be blocked");
return new BlockParticipantResult(false, "Invalid NHS Number");
}

var participantDemographic = await _participantDemographicDataService.GetSingleByFilter(x => x.NhsNumber == blockParticipantRequest.NhsNumber);

if (participantDemographic != null)
{
var recordsMatch = ValidateRecordsMatch(participantDemographic, blockParticipantRequest);
var responseBody = JsonSerializer.Serialize(new BlockParticipantDto
{
NhsNumber = participantDemographic.NhsNumber,
FamilyName = participantDemographic.FamilyName!,
DateOfBirth = participantDemographic.DateOfBirth!,
});
return new BlockParticipantResult(recordsMatch, responseBody);
}

var pdsParticipant = await GetPDSParticipant(blockParticipantRequest.NhsNumber);

if (pdsParticipant == null)
{
return new BlockParticipantResult(false, "Participant Couldn't be found");
}

var pdsRecordsMatch = ValidateRecordsMatch(pdsParticipant, blockParticipantRequest);
var pdsResponseBody = JsonSerializer.Serialize(new BlockParticipantDto
{
NhsNumber = pdsParticipant.NhsNumber,
FamilyName = pdsParticipant.FamilyName!,
DateOfBirth = pdsParticipant.DateOfBirth!
});

return new BlockParticipantResult(pdsRecordsMatch, pdsResponseBody);

}

private async Task<BlockParticipantResult> BlockNewParticipant(BlockParticipantDto blockParticipantRequest)
{
var pdsParticipant = await GetPDSParticipant(blockParticipantRequest.NhsNumber);

if (pdsParticipant == null || !ValidateRecordsMatch(pdsParticipant, blockParticipantRequest))
{
return new BlockParticipantResult(false, "Participant details do not match a records in Cohort Manager or PDS");
}

var participantManagementRecord = new ParticipantManagement
{
NHSNumber = pdsParticipant.NhsNumber,
BlockedFlag = 1,
EligibilityFlag = 0,
};

var participantManagementAdded = await _participantManagementDataService.Add(participantManagementRecord);

if (!participantManagementAdded)
{
return new BlockParticipantResult(false, "Unable to add participant to Cohort Manager to be blocked");
}

return new BlockParticipantResult(true, "Participant Has been blocked");


}

private async Task<BlockParticipantResult> BlockExistingParticipant(ParticipantManagement participant)
{
var blockFlagUpdated = await SetBlockedFlag(participant, true);

if (!blockFlagUpdated)
{
return new BlockParticipantResult(false, "Failed to Update participant in Cohort Manager");
}

var unsubscribeFromNems = await UnsubscribeParticipantFromNEMS(participant.NHSNumber);

if (!unsubscribeFromNems)
{
return new BlockParticipantResult(false, "Failed to unsubscribe Participant From NEMS");
}

return new BlockParticipantResult(true, "Participant Has been blocked");

}

private async Task<bool> UnsubscribeParticipantFromNEMS(long nhsNumber)
{
var nemsUnsubscribeResponse = await _httpClient.SendPost(_config.ManageNemsSubscriptionUnsubscribeURL, CreateNhsNumberQueryParams(nhsNumber));

return nemsUnsubscribeResponse.IsSuccessStatusCode;
}

private async Task<bool> SubscribeParticipantToNEMS(long nhsNumber)
{
var nemsSubscribeResponse = await _httpClient.SendPost(_config.ManageNemsSubscriptionSubscribeURL, CreateNhsNumberQueryParams(nhsNumber));

return nemsSubscribeResponse.IsSuccessStatusCode;
}


private async Task<bool> SetBlockedFlag(ParticipantManagement participant, bool blocked)
{
participant.BlockedFlag = blocked ? (short)1 : (short)0;
return await _participantManagementDataService.Update(participant);
}

private async Task<ParticipantDemographic> GetPDSParticipant(long nhsNumber)
{
var pdsResponse = await _httpClient.SendGet(_config.RetrievePdsDemographicURL, CreateNhsNumberQueryParams(nhsNumber));
if (string.IsNullOrEmpty(pdsResponse))
{
_logger.LogWarning("RetrievePDSDemographic Didn't return a valid response");
return null!;
}

var pdsDemographic = JsonSerializer.Deserialize<ParticipantDemographic>(pdsResponse);

return pdsDemographic!;
}

private static bool ValidateRecordsMatch(ParticipantDemographic participant, BlockParticipantDto dto)
{

if (!DateOnly.TryParseExact(dto.DateOfBirth, "yyyy-MM-dd",new CultureInfo("en-GB"),DateTimeStyles.None, out var dtoDateOfBirth ))
{
throw new FormatException("Date of Birth not in the correct format");
}

if (!DateOnly.TryParseExact(participant.DateOfBirth, "yyyyMMdd",new CultureInfo("en-GB"),DateTimeStyles.None, out var parsedDob))
{
return false;
}
return string.Equals(participant.FamilyName, dto.FamilyName, StringComparison.InvariantCultureIgnoreCase)
&& participant.NhsNumber == dto.NhsNumber
&& parsedDob == dtoDateOfBirth;
}

private static Dictionary<string, string> CreateNhsNumberQueryParams(long nhsNumber) =>
new Dictionary<string, string>
{
{"nhsNumber",nhsNumber.ToString()}
};

}
Loading
Loading