Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/build-claimservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-consentservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-locationservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-medicationdispenseservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-medicationrequestservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-medicationservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-medicationstatementservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-patientservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-practitionerservice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ env.APP }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-to-gold.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ matrix.service }}
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-to-silver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2.1.0
with:
dotnet-version: 10.0.x

- name: Building ${{ matrix.service }}
run: |
Expand Down
10 changes: 5 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Services/ClaimService/src/bin/Debug/net6.0/ClaimService.dll",
"program": "${workspaceFolder}/Services/ClaimService/src/bin/Debug/net10.0/ClaimService.dll",
"args": [],
"cwd": "${workspaceFolder}/Services/ClaimService/src/",
"stopAtEntry": false,
Expand All @@ -28,7 +28,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Services/MedicationRequestService/src/bin/Debug/net6.0/MedicationRequestService.dll",
"program": "${workspaceFolder}/Services/MedicationRequestService/src/bin/Debug/net10.0/MedicationRequestService.dll",
"args": [],
"cwd": "${workspaceFolder}/Services/MedicationRequestService/src/",
"stopAtEntry": false,
Expand All @@ -48,7 +48,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Services/MedicationStatementService/src/bin/Debug/net6.0/MedicationStatementService.dll",
"program": "${workspaceFolder}/Services/MedicationStatementService/src/bin/Debug/net10.0/MedicationStatementService.dll",
"args": [],
"cwd": "${workspaceFolder}/Services/MedicationStatementService/src/",
"stopAtEntry": false,
Expand All @@ -68,7 +68,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Services/PatientService/src/bin/Debug/net6.0/PatientService.dll",
"program": "${workspaceFolder}/Services/PatientService/src/bin/Debug/net10.0/PatientService.dll",
"args": [],
"cwd": "${workspaceFolder}/Services/PatientService/src/",
"stopAtEntry": false,
Expand All @@ -90,7 +90,7 @@
"request": "launch",
"console": "integratedTerminal",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Client/src/bin/Debug/net6.0/Client.dll",
"program": "${workspaceFolder}/Client/src/bin/Debug/net10.0/Client.dll",
"args": [],
"stopAtEntry": false,
"env": {
Expand Down
2 changes: 1 addition & 1 deletion Client/src/Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Client/src/Services/PostService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task<string> PostRequest() {
return string.Empty;
}

public FHIRMessageFormat FormatMessage(string payload) {
private FHIRMessageFormat FormatMessage(string payload) {
UTF8Encoding utf8 = new UTF8Encoding();

// Encode the payload in base64 as per HL7-v2 specifications
Expand Down
3 changes: 2 additions & 1 deletion Java-Demo/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target/*
*.key
*.key
run.bat
2 changes: 1 addition & 1 deletion Java-Demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Edit your `.key` file to include only the key, nothing else.
### Optional
To create a deployable Java program, create a `libs` folder and run:
`mvn dependency:copy-dependencies -DoutputDirectory=libs`
That will put all the dependencies into the libs folder. Then you can run it with something like:
That will put all the dependencies into the libs folder.
```
mvn compile package
java -cp "target\moh-erx-java-1.0.0.jar;./libs/*" demo.PharmaNetClaim PNET-39999999
Expand Down
24 changes: 23 additions & 1 deletion Java-Demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,33 @@
<configuration>
<archive>
<manifest>
<mainClass>gerry.PharmaNetClaim</mainClass>
<mainClass>demo.PharmaNetClaim</mainClass>
</manifest>
</archive>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>demo.PharmaNetClaim</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
27 changes: 13 additions & 14 deletions Java-Demo/src/main/java/demo/PharmaNetClaim.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,17 @@
public class PharmaNetClaim {

public static void main(String[] args) throws Exception {
boolean debug = false;
String KeycloakUrl = "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token";
String PharmaNetUrl = "https://pnet-vs1.api.gov.bc.ca/api/v1/Claim"; // different HL7 message types use

if (args.length < 1) {
System.out.println("Usage: java PharmaNetClaim <ClientId>");
System.exit(1);
}

String ClientId = args[0];
String KeycloakUrl = "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token";
String PharmaNetUrl = "https://pnet-vs1.api.gov.bc.ca/api/v1/Claim"; // different HL7 message types use
String keycloak = "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token";
// different endpoints
String Scope = "openid system/Claim.write system/Claim.read"; // different HL7 message types require different
String quick = JwtService.quick(ClientId, keycloak);
String quick = JwtService.quick(ClientId, KeycloakUrl);

String tokenRequestBody = "grant_type=client_credentials"
+ "&client_id=" + URLEncoder.encode(ClientId, StandardCharsets.UTF_8)
Expand Down Expand Up @@ -58,10 +57,11 @@ public static void main(String[] args) throws Exception {
System.out.println(tokenResponse.body());
System.exit(1);
}
// System.out.print("Keycloak access Token: " + accessToken);
if (debug)
System.out.println("Keycloak access Token: " + accessToken);

// not a valid HL7 message, but used for testing the FHIR envelope and PharmaNet response
String hl7 = "MSH|^~&|DESKTOP|PNET-39999999|PNP|PP||GERRYWASHERE,,WL*E5R:SS0AR|ZPN|631708|P|2.1||\n"
// not a valid HL7 message
String hl7 = "MSH|^~&|DESKTOP|" + ClientId + "|PNP|PP||GERRYWASHERE,,WL*E5R:SS0AR|ZPN|631708|P|2.1||\n"
+ "ZCA|000001|03|00|AR|04\n"
+ "ZCB|BC00000000|260806|631708\n"
+ "ZZZ|TDR||631708|P1|07963|||";
Expand Down Expand Up @@ -99,16 +99,15 @@ public static void main(String[] args) throws Exception {
.build();

HttpResponse<String> claimResponse = client.send(claimRequest, HttpResponse.BodyHandlers.ofString());

JsonNode responseJson = objectMapper.readTree(claimResponse.body());
String responseData = responseJson.path("content").path(0).path("attachment").path("data").asText();

if (responseData.isEmpty()) {
System.out.println("Unexpected response from PharmaNet, no data payload received");
System.out.println(claimResponse.body());
System.exit(1);
System.out.println("HL7v2 Authorization Failed, transaction ignored.");
return;
}

if (debug)
System.out.println(claimResponse.body());
String hl7Response = new String(Base64.getDecoder().decode(responseData), "UTF-8");
System.out.println(hl7Response.replace("\r", "\n"));
}
Expand Down
97 changes: 97 additions & 0 deletions README.deverloper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Developer README

This repository contains a set of ASP.NET Core services that expose FHIR-style endpoints and translate them into PharmaNet HL7 v2 transactions. The important thing to understand is that the API layer is intentionally thin: controllers accept incoming requests, shared infrastructure parses and authorizes them, and a downstream service forwards the payload to PharmaNet.

## What developers need to know first

Most services in this repo follow the same pattern:

1. A resource-specific controller receives an HTTP request.
2. A shared base controller reads the raw body, parses the incoming FHIR payload, and extracts the embedded HL7 v2 message.
3. Authorization validates the caller before anything is sent upstream.
4. A shared service layer converts the request into the outbound Pharmanet message model and hands it to the delegate layer.
5. The response is transformed back into a FHIR document and returned to the client.

## Authorization checks

The authorization layer is implemented in the shared common code under Services/Common/src and is applied before the request is forwarded to PharmaNet.

### Required authentication

Every protected endpoint expects a valid JWT bearer token issued by the configured OpenID Connect provider. In practice, that means:

- the caller must present an Authorization: Bearer <token> header;
- the token must be signed and validated by the configured authority;
- the token must be accepted by the configured audience and issuer settings.

If the token is missing, invalid, expired, or otherwise rejected by ASP.NET Core authentication, the request is treated as unauthenticated and authorization fails.

### Scope checks

The main authorization checks are scope-based:

- FHIR-level policy checks ensure the caller has at least one of the configured scopes from the OpenID Connect settings.
- HL7 v2 message-level checks inspect the parsed HL7 message and determine which scopes are required for that message type and message shape.
- The required scopes are configured in the application configuration under the HL7 v2 authorization section, and the handler compares those values against the scopes present in the JWT.

In other words, the service does not just check that a token exists; it checks that the token contains the right scope claims for the operation being requested.

### What is not enforced here

From the code in the current implementation, there are no explicit checks for:

- client IP address or source network range;
- pharmacy ID;
- facility ID;
- practitioner ID;
- organization or tenant membership.

The current authorization logic is centered on authentication and scope claims. If those controls are needed in the future, they would need to be added explicitly in the shared authorization handlers or in the upstream gateway layer.

### How the checks work in practice

The flow is roughly:

1. ASP.NET Core authenticates the JWT.
2. The FHIR scope policy checks that the user is authenticated and has at least one allowed scope.
3. The HL7 v2 authorization handler parses the embedded HL7 message and determines the scopes required for that message type.
4. If the JWT contains one of those required scopes, the request is allowed to proceed; otherwise authorization fails.

## Main classes and responsibilities

- Program and Startup: service entry points and ASP.NET Core startup wiring.
- MedicationRequestController (and similar resource controllers): resource-specific endpoints and routing.
- ServiceBaseController: shared request pipeline for parsing, authorization, and response formatting.
- Hl7Parser: converts between FHIR JSON and HL7 v2 messages.
- PharmanetService: orchestration layer between the API layer and downstream transport.
- IPharmanetDelegate / PharmanetDelegate: outbound integration with the PharmaNet proxy.
- Authorization policy and requirement classes: enforce FHIR and HL7 scope checks.

## Repository layout

- Services/eRXAPI.sln: the main backend solution.
- Services/MedicationRequestService/src: the MedicationRequest service implementation used as the reference pattern.
- Services/Common/src: shared ASP.NET setup, controller base classes, parsers, authorization logic, and transport code.
- Client/src: a sample client that shows how to authenticate and call the APIs.

## Running locally

From the repo root, you can build the main solution with:

```bash
dotnet build Services/eRXAPI.sln
```

To run the MedicationRequest service locally:

```bash
dotnet run --project Services/MedicationRequestService/src/MedicationRequestService.csproj
```

The service exposes resource endpoints under the standard API route pattern and health checks under /healthz for the service-specific controllers.

## Contribution notes

- Prefer changing shared behavior in Services/Common/src rather than duplicating logic in each service.
- Keep controllers thin; most request handling should live in the shared base controller and service layer.
- If you change the request or response contract, update the related service and the sample client as well.
2 changes: 1 addition & 1 deletion Services/ClaimService/src/ClaimService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<RootNamespace>Health.PharmaNet.ClaimService</RootNamespace>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(MSBuildThisFileName).xml</DocumentationFile>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="health-check.sh" CopyToOutputDirectory="Always" />
Expand Down
9 changes: 4 additions & 5 deletions Services/ClaimService/src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0
FROM mcr.microsoft.com/dotnet/aspnet:10.0
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
RUN apt-get update -y && \
apt-get install -y curl telnet
RUN apt-get update -y && apt-get install -y curl telnet
RUN dpkg -r --force-all apt apt-get && dpkg -r --force-all debconf dpkg
WORKDIR /app
COPY . .
RUN useradd --user-group --system --create-home --no-log-init app
COPY out/ .
RUN id app &>/dev/null || useradd --user-group --system --create-home --no-log-init app
USER app
EXPOSE 8080
ENTRYPOINT ["dotnet","ClaimService.dll"]
Loading
Loading