Skip to content

Commit ec335c3

Browse files
sudipnhsMacMur85
andauthored
feat: DTOSS-8379 new functionapp for creating nems subscription (#967)
* feat: DTOSS-8379 new function app NEMSSubscribe * feat: DTOSS-8379 config added * feat: DTOSS-8379 compose file * feat: terraform config for the new function * feat: terraform config for the new function * feat: DTOSS-8379 unused method removed * feat: DTOSS-8379 XML doc required * feat: DTOSS-8379 unit test renamed * feat: DTOSS-8379 moved NEMSSubscribe from shared folder * feat: DTOSS-8379 resolve .tfvars conflicts * feat: DTOSS-8379 modified httpclientfunction * feat: DTOSS-8379 modified httpclientfunction and its interface * feat: DTOSS-8379 refactor * feat: DTOSS-8379 refactor code * feat: DTOSS-8379 Unit tests * feat: DTOSS-8379 unit test added * feat: DTOSS-8379 unit test fix1 * feat: DTOSS-8379 unit test fix for createresponsemock * feat: DTOSS-8379 removed unused method * feat: DTOSS-8379 resolve conflicts2 * feat: DTOSS-8379 resolve conflicts3 * feat: DTOSS-8379 deleting devtest.tfvars file * feat: DTOSS-8379 PR comments resolved --------- Co-authored-by: Maciej Murawski <[email protected]>
1 parent c306038 commit ec335c3

File tree

14 files changed

+584
-1283
lines changed

14 files changed

+584
-1283
lines changed

application/CohortManager/compose.core.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ name: cohort-manager
33
services:
44
# CaaS Integration Service
55

6-
76
retrieve-mesh-file:
87
container_name: retrieve-mesh-file
98
image: cohort-manager-retrieve-mesh-file
@@ -329,6 +328,20 @@ services:
329328
- ExceptionFunctionURL=http://create-exception:7070/api/CreateException
330329
- RetrievePdsParticipantURL=https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient
331330

331+
nems-subscribe:
332+
container_name: nems-subscribe
333+
image: cohort-manager-nems-subscribe
334+
networks: [cohman-network]
335+
build:
336+
context: ./src/Functions/
337+
dockerfile: DemographicServices/NEMSSubscribe/Dockerfile
338+
profiles: [not-implemented]
339+
environment:
340+
- ASPNETCORE_URLS=http://*:9081
341+
- ExceptionFunctionURL=http://create-exception:7070/api/CreateException
342+
- ParticipantDemographicDataServiceURL=http://participant-demographic-data-service:7993/api/ParticipantDemographicDataService
343+
- RetrievePdsDemographicURL=http://retrieve-pds-demographic:8082/api/RetrievePDSDemographic
344+
332345
nems-unsubscribe:
333346
container_name: nems-unsubscribe
334347
image: cohort-manager-nems-unsubscribe
@@ -357,7 +370,7 @@ services:
357370
- AzureWebJobsStorage=${AZURITE_CONNECTION_STRING}
358371
- AcceptableLatencyThresholdMs=500
359372

360-
# UI
373+
# UI
361374
web:
362375
container_name: web
363376
build:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base
2+
3+
COPY ./Shared /Shared
4+
WORKDIR /Shared
5+
6+
RUN mkdir -p /home/site/wwwroot && \
7+
dotnet publish ./Common/Common.csproj --output /home/site/wwwroot && \
8+
dotnet publish ./Model/Model.csproj --output /home/site/wwwroot && \
9+
dotnet publish ./Data/Data.csproj --output /home/site/wwwroot && \
10+
dotnet publish ./Utilities/Utilities.csproj --output /home/site/wwwroot && \
11+
dotnet publish ./DataServices.Client/DataServices.Client.csproj --output /home/site/wwwroot && \
12+
dotnet publish ./DataServices.Core/DataServices.Core.csproj --output /home/site/wwwroot && \
13+
dotnet publish ./DataServices.Database/DataServices.Database.csproj --output /home/site/wwwroot
14+
15+
FROM base AS function
16+
17+
COPY ./DemographicServices/NEMSSubscribe /src/dotnet-function-app
18+
WORKDIR /src/dotnet-function-app
19+
20+
RUN dotnet publish *.csproj --output /home/site/wwwroot
21+
22+
# To enable ssh & remote debugging on app service change the base image to the one below
23+
# FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0-appservice
24+
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0
25+
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
26+
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
27+
28+
COPY --from=function ["/home/site/wwwroot", "/home/site/wwwroot"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace NHS.CohortManager.NEMSSubscriptionFunction;
2+
3+
using System.Net;
4+
using System.Threading.Tasks;
5+
using HealthChecks.Extensions;
6+
using Microsoft.Azure.Functions.Worker;
7+
using Microsoft.Azure.Functions.Worker.Http;
8+
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
10+
public class HealthCheckFunction
11+
{
12+
private readonly HealthCheckService _healthCheckService;
13+
14+
public HealthCheckFunction(HealthCheckService healthCheckService)
15+
{
16+
_healthCheckService = healthCheckService;
17+
}
18+
19+
[Function("health")]
20+
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
21+
{
22+
return await HealthCheckServiceExtensions.CreateHealthCheckResponseAsync(req, _healthCheckService);
23+
}
24+
}
25+
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
namespace NHS.CohortManager.DemographicServices;
2+
3+
using System;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Text;
7+
using System.IO;
8+
using System.Text.Json;
9+
using System.Threading.Tasks;
10+
using Microsoft.Azure.Functions.Worker;
11+
using Microsoft.Azure.Functions.Worker.Http;
12+
using Microsoft.Extensions.Logging;
13+
using Hl7.Fhir.Model;
14+
using Hl7.Fhir.Serialization;
15+
using Hl7.Fhir.Rest;
16+
using Microsoft.Extensions.Options;
17+
using Model;
18+
using Common;
19+
using DataServices.Client;
20+
using Common.Interfaces;
21+
22+
public class NEMSSubscribe
23+
{
24+
private readonly ILogger<NEMSSubscribe> _logger;
25+
private readonly IHttpClientFunction _httpClientFunction;
26+
private readonly ICreateResponse _createResponse;
27+
private readonly NEMSSubscribeConfig _config;
28+
private readonly IDataServiceClient<NemsSubscription> _nemsSubscriptionClient;
29+
private const string urlFormat = "{0}/{1}";
30+
31+
public NEMSSubscribe
32+
(
33+
ILogger<NEMSSubscribe> logger,
34+
IDataServiceClient<NemsSubscription> nemsSubscriptionClient,
35+
IHttpClientFunction httpClientFunction,
36+
ICreateResponse createResponse,
37+
IOptions<NEMSSubscribeConfig> nemsSubscribeConfig
38+
)
39+
{
40+
_logger = logger;
41+
_nemsSubscriptionClient = nemsSubscriptionClient;
42+
_httpClientFunction = httpClientFunction;
43+
_createResponse = createResponse;
44+
_config = nemsSubscribeConfig.Value;
45+
}
46+
47+
/// <summary>
48+
/// Azure Function that processes a NEMS subscription request by validating the NHS number,
49+
/// verifying the patient in PDS, creating a FHIR subscription resource, posting it to NEMS,
50+
/// and attempting to store the subscription locally in Sql-Server.
51+
/// </summary>
52+
/// <param name="req">
53+
/// The HTTP request data containing a JSON payload with the NHS number to subscribe.
54+
/// </param>
55+
/// <returns>
56+
/// An <see cref="HttpResponseData"/> indicating success (200 OK) with the subscription ID,
57+
/// or failure with the appropriate HTTP status code and error message.
58+
/// </returns>
59+
/// <remarks>
60+
/// This function performs the following steps:
61+
/// 1. Validates the NHS number format.
62+
/// 2. Calls PDS by calling "RetrievePDSDemographic" function to confirm patient existence.
63+
/// 3. Constructs and sends a FHIR subscription resource to NEMS.
64+
/// 4. After successful subscription creation, it will store the subscription details like
65+
/// subscriptionId, Subscription Logic Id etc locally in Sql-Server.
66+
/// </remarks>
67+
/// <exception cref="Exception">
68+
/// Returns HTTP 500 if an unexpected error occurs during processing.
69+
/// </exception>
70+
71+
[Function("NEMSSubscribe")]
72+
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "subscriptions/nems")] HttpRequestData req)
73+
{
74+
try
75+
{
76+
var nhsNumber = req.Query["nhsNumber"];
77+
78+
// 1. Create Subscription Resource
79+
Subscription subscription = CreateNemsSubscriptionResource(nhsNumber);
80+
var subscriptionJson = new FhirJsonSerializer().SerializeToString(subscription);
81+
82+
// 2. Post to NEMS FHIR endpoint
83+
string subscriptionId = await PostSubscriptionToNems(subscriptionJson);
84+
if (string.IsNullOrEmpty(subscriptionId))
85+
{
86+
_logger.LogError("Failed to create subscription in NEMS.");
87+
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req, "Failed to create subscription in NEMS.");
88+
}
89+
90+
// 3. Store in SQL Database
91+
bool storageSuccess = await StoreSubscriptionInDatabase(nhsNumber, subscriptionId);
92+
if (!storageSuccess)
93+
{
94+
_logger.LogError("Subscription created but failed to store locally.");
95+
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req, "Subscription created but failed to store locally.");
96+
}
97+
98+
return _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, subscriptionId);
99+
}
100+
catch (Exception ex)
101+
{
102+
_logger.LogError(ex, "Error in NEMS subscription workflow: {Message}", ex.Message);
103+
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
104+
}
105+
}
106+
107+
public async Task<string> PostSubscriptionToNems(string subscription)
108+
{
109+
/* This is a WIP as additional work is required to use the NEMS endpoint after onboarding to NemsApi hub. */
110+
try
111+
{
112+
113+
// POST to NEMS
114+
var url = string.Format(urlFormat, _config.NemsFhirEndpoint, "Subscription");
115+
var response = await _httpClientFunction.PostNemsGet(
116+
url,
117+
subscription,
118+
_config.SpineAccessToken,
119+
_config.FromAsid,
120+
_config.ToAsid
121+
);
122+
123+
if (!response.IsSuccessStatusCode)
124+
{
125+
_logger.LogError($"NEMS subscription failed: {response.StatusCode}");
126+
return null;
127+
}
128+
129+
var responseContent = await response.Content.ReadAsStringAsync();
130+
return Guid.NewGuid().ToString();
131+
}
132+
catch (Exception ex)
133+
{
134+
_logger.LogError(ex, "NEMS subscription error: {Message}", ex.Message);
135+
return null;
136+
}
137+
}
138+
139+
public async Task<bool> StoreSubscriptionInDatabase(string nhsNumber, string subscriptionId)
140+
{
141+
/* This is a WIP as additional work is required to use the NEMS endpoint after onboarding to NemsApi hub. */
142+
_logger.LogInformation("Start saving the SubscriptionId in the database.");
143+
var objNemsSubscription = new NemsSubscription
144+
{
145+
SubscriptionId = Guid.Parse(subscriptionId), //WIP , might change after onboarding as datatype might change
146+
NhsNumber = Convert.ToInt64(nhsNumber), //WIP , might change after onboarding as datatype might change
147+
RecordInsertDateTime = DateTime.UtcNow
148+
};
149+
var subscriptionCreated = await _nemsSubscriptionClient.Add(objNemsSubscription);
150+
151+
if (subscriptionCreated)
152+
{
153+
_logger.LogInformation("Successfully created the subscription");
154+
return true;
155+
}
156+
_logger.LogError("Failed to create the subscription");
157+
return false;
158+
}
159+
160+
public Subscription CreateNemsSubscriptionResource(string nhsNumber)
161+
{
162+
/* This is a WIP as additional work is required to use the NEMS endpoint after onboarding to NemsApi hub. */
163+
var subscription = new Subscription
164+
{
165+
Meta = new Meta
166+
{
167+
//Profile = new[] { "https://fhir.nhs.uk/StructureDefinition/EMS-Subscription-1" }, // WIP, Will remove this after onboarding
168+
Profile = new[] { _config.SubscriptionProfile },
169+
LastUpdated = DateTimeOffset.UtcNow
170+
},
171+
Status = Subscription.SubscriptionStatus.Requested,
172+
Reason = "NEMS event notification subscription",
173+
//Criteria = $"Patient?identifier=https://fhir.nhs.uk/Id/nhs-number|{nhsNumber}", // WIP, Will remove this after onboarding
174+
Criteria = $"Patient?identifier={_config.SubscriptionCriteria}|{nhsNumber}",
175+
Channel = new Subscription.ChannelComponent
176+
{
177+
Type = Subscription.SubscriptionChannelType.RestHook,
178+
Endpoint = _config.CallbackEndpoint,
179+
Payload = "application/fhir+json",
180+
Header = new[] {
181+
$"Authorization: Bearer {_config.CallAuthToken}",
182+
"X-Correlation-ID: " + Guid.NewGuid().ToString()
183+
}
184+
}
185+
};
186+
187+
return subscription;
188+
}
189+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
5+
<OutputType>Exe</OutputType>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
11+
<PackageReference Include="Hl7.Fhir.R4" Version="5.11.4" />
12+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
13+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
14+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />
15+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
16+
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
17+
</ItemGroup>
18+
<ItemGroup>
19+
<None Update="host.json">
20+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
21+
</None>
22+
<None Update="local.settings.json">
23+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
24+
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
25+
</None>
26+
</ItemGroup>
27+
<ItemGroup>
28+
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
29+
</ItemGroup>
30+
<ItemGroup>
31+
<ProjectReference Include="..\..\Shared\Common\Common.csproj" />
32+
<ProjectReference Include="..\..\Shared\Data\Data.csproj" />
33+
</ItemGroup>
34+
<ItemGroup>
35+
<ProjectReference Include="..\..\Shared\HealthChecks\HealthChecks.csproj" />
36+
<ProjectReference Include="..\DataServices.Core\DataServices.Core.csproj" />
37+
</ItemGroup>
38+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace NHS.CohortManager.DemographicServices;
2+
3+
using System.ComponentModel.DataAnnotations;
4+
5+
public class NEMSSubscribeConfig
6+
{
7+
[Required]
8+
public string NemsFhirEndpoint { get; set; }
9+
10+
[Required]
11+
public string RetrievePdsDemographicURL { get; set; }
12+
public string SpineAccessToken { get; set; }
13+
public string FromAsid { get; set; }
14+
public string ToAsid { get; set; }
15+
public string SubscriptionProfile { get; set; }
16+
public string SubscriptionCriteria { get; set; }
17+
public string CallbackEndpoint { get; set; }
18+
public string CallAuthToken { get; set; }
19+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using DataServices.Client;
2+
using HealthChecks.Extensions;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Common;
6+
using NHS.CohortManager.DemographicServices;
7+
using Model;
8+
9+
var host = new HostBuilder()
10+
.AddConfiguration<NEMSSubscribeConfig>(out NEMSSubscribeConfig config)
11+
.AddDataServicesHandler()
12+
.Build()
13+
.ConfigureFunctionsWebApplication()
14+
.ConfigureServices(services =>
15+
{
16+
services.AddSingleton<ICreateResponse, CreateResponse>();
17+
services.AddHttpClient();
18+
services.AddScoped<IHttpClientFunction, HttpClientFunction>();
19+
// Register health checks
20+
services.AddBasicHealthCheck("NEMSSubscription");
21+
})
22+
.Build();
23+
24+
await host.RunAsync();

0 commit comments

Comments
 (0)