diff --git a/doc/developer_information.md b/doc/developer_information.md new file mode 100644 index 00000000..2e077988 --- /dev/null +++ b/doc/developer_information.md @@ -0,0 +1,242 @@ +# NHS 111 Integration Adaptor + +## Description + +The NHS 111 Integration Adaptor enables GP Practices to receive NHS 111 Report messages from the NHS 111 service, routing a "post event message" to the relevant GP following a patient's call to the NHS 111 service. This covers ambulance call outs, referrals to a local emergency department, referrals to an Out of Hours service, and referrals to the patient's own GP. + +The adaptor receives ITK 2.2 wrapped Clinical Document Architecture (CDA) XML documents over web services and converts them into structured FHIR messages, which it posts onto the GP system's inbound event queue (an AMQP-based message queue, configurable for either the version 1-0-0 protocol or RabbitMQ protocol 0-9-1) within the GP supplier's own environment. The adaptor does not store messages itself. + +It integrates with: +- **The NHS 111 service** — the inbound source of ITK/CDA messages over SOAP, secured with TLS Mutual Authentication +- **An optional external configuration service** — for ODS code / DOS Service ID validation lists +- **The GP supplier's AMQP queue** — the outbound destination + +## Prerequisites + +- **JDK 21** is required to build and run the adaptor service (Gradle build) +- **Docker** (with docker-compose) is required to build and run the adaptor as containers +- **Gradle** is used via the wrapper (`./gradlew`) from the service directory for building, unit tests, and integration tests; no separate Gradle install is required + +> **Note:** the project source is predominantly Java (Gradle-based), with smaller TypeScript, Shell, and JavaScript components and no additional language runtimes beyond JDK 21 are indicated. + +## Specifications + +The following specification should be suitable for production deployment, but memory / compute usage should be monitored during usage: + +| Resource | Specification | +|---|---| +| Compute | 2-4 vCPU (dependent on message throughput) | +| VRAM | 2-4 GB (JVM min 512MB, max 2GB recommended) | +| Storage | 20 GB minimum (for Docker images and logs) | + +## Design + +### Components + +- **Nginx proxy** — handles TLS Mutual Authentication for inbound NHS 111 traffic. Configured via environment variables for server/client certificates, CA cert, and certificate revocation list (CRL), including support for downloading the CRL on startup. +- **Adaptor service** — receives ITK 2.2-wrapped CDA XML over SOAP at a `/report` endpoint, validates it, and converts it to structured FHIR before posting to the outbound AMQP queue. +- **AMQP message queue** — the outbound message queue within the GP supplier's environment, to which converted FHIR post event messages are sent. The adaptor does not persist messages itself. +- **Optional external configuration service** — if configured (`PEM111_ITK_EXTERNAL_CONFIGURATION_URL`), the adaptor polls this service for valid ODS codes and DOS Service IDs, reloading without downtime. + +## Implementation + +The adaptor accepts ITK 2.2-wrapped CDA (Clinical Document Architecture) XML requests over SOAP at a `/report` endpoint, fronted by Nginx which terminates TLS Mutual Authentication. It supports four NHS 111 referral outcomes: ambulance call out, referral to a local emergency department, referral to an Out of Hours service, and referral to the patient's own GP. + +Validated messages are converted from CDA into structured FHIR and posted onto the GP system's inbound AMQP queue; the adaptor holds no message store itself. + +### Request flow + +1. NHS 111 sends the SOAP/ITK message to `/report` +2. Nginx authenticates via TLS Mutual Authentication +3. The adaptor optionally validates the SOAP `To` field +4. The adaptor validates the ITK header's ODS code and/or DOS Service ID against a static list or an externally polled list (accepted if either matches) +5. The CDA document is converted to FHIR +6. The FHIR message is posted to the AMQP queue + +### Supported ITK interactions + +- An ambulance call out +- A referral to a local emergency department +- A referral to an Out Of Hours service +- A referral to their GP + +### Key environment variables + +**Inbound queue:** + +| Variable | Description | +|---|---| +| `PEM111_AMQP_BROKER` | Name of the AMQP broker (default: `amqp://activemq:5672`) | +| `PEM111_AMQP_QUEUE_NAME` | Name of the queue | +| `PEM111_AMQP_USERNAME` | Queue username | +| `PEM111_AMQP_PASSWORD` | Queue password | +| `PEM111_AMQP_PORT` | AMQP port (default: `5672`) | +| `PEM111_AMQP_PROTOCOL` | AMQP protocol (set to `0-9-1` for RabbitMQ) | +| `PEM111_AMQP_ROUTING_KEY` | AMQP routing key | +| `PEM111_AMQP_SSL_ENABLED` | Whether AMQP SSL is enabled (default: `false`) | + +**SOAP ITK:** + +| Variable | Description | +|---|---| +| `PEM111_SOAP_VALIDATION_ENABLED` | Whether incoming SOAP ITK messages can be validated | +| `PEM111_SOAP_SEND_TO` | Provided validation is on, this is the `/report` endpoint URL used to set the expected value | + +**ITK header/ODS-DOS:** + +| Variable | Description | +|---|---| +| `PEM111_ITK_ODS_CODE_LIST` | ODS code list (default: `EM396`) | +| `PEM111_ITK_DOS_ID_LIST` | DOS ID list | +| `PEM111_ITK_EXTERNAL_CONFIGURATION_URL` | ITK external config URL (default: `http://wiremock:8080/configuration/ods-dos`) | +| `PEM111_ITK_FETCH_INTERVAL_MIN` | ITK fetch interval in minutes (default: `5`) | + +> **Note:** At least one of the three list/URL variables must be set or the adaptor fails to start; an external URL overrides static lists and is validated on startup. + +**TLS Mutual Authentication:** + +| Variable | Description | +|---|---| +| `NGINX_PUBLIC_CERT` | Server public certificate | +| `NGINX_PRIVATE_CERT` | Server private certificate | +| `NGINX_CLIENT_PUBLIC_CERT` | Client public certificate | +| `NGINX_CA_CERT` | Root certificate | +| `NGINX_CRL` | Certificate revocation list | +| `NGINX_CRL_URL` | CRL URL (Nginx can download CRL on startup) | + +**Locally:** AMQP queue name `encounter-report`, broker `amqp://activemq:5672`. + +The adaptor is published and deployed as two Docker images (proxy and adaptor) within the consuming organisation's own environment. + +## Running the adaptor through the IDE (IntelliJ) + +1. Open the project in IntelliJ. +2. Go to **File → Project Structure → Project → SDK** and select JDK 21 (Corretto or Temurin). +3. Go to **File → Project Structure → Modules** → find `service.integration-adaptor-111.main` and set the SDK/language level to 21. +4. Open the Gradle panel (elephant icon on the right toolbar) and hit the refresh/sync button to ensure the project is indexed. +5. Open **Run/Debug Configurations** (top right dropdown → Edit Configurations) and create a new Application configuration. +6. Set the **Name** to `OneOneOneApplication`. +7. In the **-cp module** dropdown, select `service.integration-adaptor-111.main`. +8. In the **Main class** field, enter `uk.nhs.adaptors.oneoneone.OneOneOneApplication`. +9. Click **OK** to save. +10. Before running, start the local broker by navigating to the `docker` directory and running `./start-local-environment.sh`. +11. Once the broker is up, hit the green play button in the top right toolbar to run the adaptor. + +## Running the adaptor without Nginx + +With the adaptor running, navigate to `test-suite` and run `./start-local.sh`. Open the local instance of ActiveMQ in a browser. Go back to the localhost page which popped up on `http://localhost:3000/` and click **Run Tests**. A message should have been queued on the ActiveMQ queue and its message ID should match the 200 OK response you got from the POST action. + +## How to run tests (unit and integration) + +### Via command line + +Navigate to the service directory: `cd service` + +- `./gradlew test` — runs the unit tests +- `./gradlew integrationTest` — runs the integration tests +- `./gradlew clean` — clears Gradle's build cache + +### Via IntelliJ + +Open the Gradle panel (elephant icon on the right) → expand **Tasks > verification**. + +Double-click **test** to run unit tests, or **integrationTest** to run integration tests. + +## Using static analysis tools + +This repo uses Checkstyle and SpotBugs, configured via Gradle plugins in `service/build.gradle` (`id "checkstyle"`, `id "com.github.spotbugs"`). + +Run locally (from `service/`): + +```bash +./gradlew checkstyleMain checkstyleTest +./gradlew spotbugsMain +./gradlew check +``` + +## Caveats + +The adaptor doesn't store or persist any messages itself, meaning it just receives, validates, converts, and forwards them straight to the AMQP queue. So there's no way to go back and inspect a message if something fails halfway through — you're relying on logs and whatever did or didn't arrive on the queue at the time. + +It also means retry logic can't live on the adaptor's side, so if a send fails, there's nothing sitting there to retry — that responsibility falls to NHS 111 resending or whatever's consuming the queue. + +## End-to-end testing + +E2E testing is done via the `test-suite` (`test-suite/`, separate from the Gradle-based `service/`), which is a Node.js client/server app for sending ITK PEM test messages against a running instance of the adaptor. + +> **Note:** if you don't have Node.js installed as a CLI, you'll get an error, so install it according to the requirements of your OS. + +To run the testing scenarios locally, go into the `test-suite` folder and run the script: + +```bash +cd test-suite +sh start-local.sh +``` + +This starts both the client and server. The client opens in your browser automatically, or you can reach it manually at `http://localhost:3000`. The backend server runs on port 7070, and the client is already hard-coded to talk to it, so you don't need to manage that connection yourself. You can confirm the backend is up by running this in a separate terminal: + +```bash +curl -v http://localhost:7070/healthcheck +``` + +To run a test, go to the browser UI, scroll to the test you want, click **Run Test**. This will display a form, which you can send as is, or adjust the values according to your specific test requirements. Finally, click **Send**, and wait for the test to complete. + +## Debugging + +**Debugging locally:** Start the dependencies with: + +```bash +cd docker && docker compose up -d activemq wiremock rabbitmq +``` + +Then run the main application class in IntelliJ using Debug. Check the app is up with: + +```bash +curl http://localhost:8080/actuator/health +``` + +Then set breakpoints and test the code by sending a message to the adaptor's inbound endpoint. + +**First time only:** if compose complains the network is missing, run: + +```bash +docker network create 111network +``` + +**Docker logs:** `docker compose logs -f` from the `docker/` directory, or add a service name for a single container. + +## CI/CD pipeline using GitHub Actions + +The pipeline (`.github/workflows/build.yml`) runs on two events: when a pull request is opened, synced, or reopened against `main`, and on every push to `main`. + +### Description of stages + +1. **Checkstyle and SpotBugs** run first in parallel. Checkstyle lints the code style, SpotBugs runs static analysis for bug patterns. Both run against main, test, and integration test sources, and upload their reports as build artifacts regardless of pass/fail. + +2. **Unit tests and integration tests** only run once both Checkstyle and SpotBugs have passed. They also run in parallel: + - Unit tests run `./gradlew test jacocoTestReport`. + - Integration tests spin up Docker dependencies first (ActiveMQ and WireMock, on a dedicated `111network` Docker network), then run `./gradlew integrationTest`. Docker logs and test reports are collected as artifacts either way, and the containers are torn down afterward regardless of outcome. + +3. **Generate build ID** runs once both test jobs succeed. It builds a unique tag based on whether the trigger was a PR or a push to `main`, the GitHub Actions run number, and the commit SHA. + +4. **Build & publish Docker images** builds two images, one for the adaptor service and one for Nginx, tags them with the generated build ID, and pushes them to AWS ECR. This step assumes an AWS IAM role via OIDC (`configure-aws-credentials`) rather than static credentials. Critically, this means every pull request gets its own build pushed to AWS ECR with a unique build ID, so a reviewer can pull and test the exact code in that PR without needing to merge it first or build anything locally. + +5. **Comment on PR** (PR events only) posts a comment back on the pull request confirming the build ID that was just built and published. + +## Releasing and versioning + +1. Ensure the `CHANGELOG` file is up to date and that the unreleased commits are classified under the correct type of change (e.g. `#Fixed` for bugs and so on). +2. Click on **Releases** and **Draft new release**. +3. Consider whether the release is a major or a minor one, and carry on semantic versioning accordingly, keeping in mind what this means for customers who will have to upgrade to the new release. +4. Specify a tag version to use (e.g. `0.11`), with the target being the latest commit using the options available. +5. Click on **Generate release notes** — this will list all the current changes from the recent commit. + +## Links / Additional information + +**Published images:** +- Adaptor: [hub.docker.com/r/nhsdev/nia-111-adaptor](https://hub.docker.com/r/nhsdev/nia-111-adaptor) +- Nginx proxy: [hub.docker.com/r/nhsdev/nia-111-nginx-adaptor](https://hub.docker.com/r/nhsdev/nia-111-nginx-adaptor) + +**Resources:** +- [NHS 111 API Catalogue](https://digital.nhs.uk/developer/api-catalogue/nhs-111) +- [Interoperability Toolkit](https://digital.nhs.uk/services/interoperability-toolkit) \ No newline at end of file diff --git a/test-suite/start-local.sh b/test-suite/start-local.sh old mode 100644 new mode 100755