diff --git a/.editorconfig b/.editorconfig index 5e6df4b4f..313568664 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,18 +11,8 @@ end_of_line = lf [Makefile] indent_style = tab -[*.{xml,js,json,yaml}] +[*.{xml,js,json,yaml,yml}] indent_size = 2 [*.postman_collection.json] indent_style = tab - -[*.yaml] -trim_trailing_whitespace = false - -[*.js] -ij_javascript_force_semicolon_style = true -ij_javascript_use_semicolon_after_statement = false - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d7cd68d7f..e87cdcb62 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser: [e.g. chrome, safari] - - Version: [e.g. 22] + +- OS: [e.g. iOS] +- Browser: [e.g. chrome, safari] +- Version: [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser: [e.g. stock browser, safari] - - Version: [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser: [e.g. stock browser, safari] +- Version: [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d6..2bc5d5f71 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 864f27b7d..56632d31b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,24 @@ ## Summary -* Routine Change -* :exclamation: Breaking Change -* :robot: Operational or Infrastructure Change -* :sparkles: New Feature -* :warning: Potential issues that might be caused by this change -Add any other relevant notes or explanations here. **Remove this line if you have nothing to add.** +- Routine Change +- :exclamation: Breaking Change +- :robot: Operational or Infrastructure Change +- :sparkles: New Feature +- :warning: Potential issues that might be caused by this change +Add any other relevant notes or explanations here. **Remove this line if you have nothing to add.** ## Reviews Required -* [x] Dev -* [ ] Test -* [ ] Tech Author -* [ ] Product Owner +- [x] Dev +- [ ] Test +- [ ] Tech Author +- [ ] Product Owner ## Review Checklist + :information_source: This section is to be filled in by the **reviewer**. -* [ ] I have reviewed the changes in this PR and they fill all or part of the acceptance criteria of the ticket, and the code is in a mergeable state. -* [ ] If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work. -* [ ] I have ensured the changelog has been updated by the submitter, if necessary. +- [ ] I have reviewed the changes in this PR and they fill all or part of the acceptance criteria of the ticket, and the code is in a mergeable state. +- [ ] If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work. +- [ ] I have ensured the changelog has been updated by the submitter, if necessary. diff --git a/.github/workflows/create-release-tag.yml b/.github/workflows/create-release-tag.yml index 5d990883b..241cf502a 100644 --- a/.github/workflows/create-release-tag.yml +++ b/.github/workflows/create-release-tag.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout uses: actions/checkout@v5 with: - fetch-depth: 0 # This causes all history to be fetched, which is required for calculate-version to function + fetch-depth: 0 # This causes all history to be fetched, which is required for calculate-version to function - name: Install Python 3.9 uses: actions/setup-python@v6 @@ -20,7 +20,7 @@ jobs: python-version: 3.9 - name: Upgrade python pip - run: python -m pip install --upgrade pip + run: python -m pip install --upgrade pip - name: Install git run: pip install gitpython diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 48e3e3e03..b9790a68b 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -136,11 +136,11 @@ jobs: if: ${{ inputs.environment == 'dev' && inputs.create_mns_subscription }} with: python-version: 3.11 - cache: 'poetry' + cache: "poetry" - name: Create MNS Subscription if: ${{ inputs.environment == 'dev' && inputs.create_mns_subscription }} - working-directory: './lambdas/mns_subscription' + working-directory: "./lambdas/mns_subscription" env: APIGEE_ENVIRONMENT: ${{ inputs.apigee_environment }} SQS_ARN: ${{ env.ID_SYNC_QUEUE_ARN }} diff --git a/.github/workflows/pr-teardown.yml b/.github/workflows/pr-teardown.yml index aae15ebb8..8830234ff 100644 --- a/.github/workflows/pr-teardown.yml +++ b/.github/workflows/pr-teardown.yml @@ -23,7 +23,7 @@ jobs: permissions: id-token: write contents: read - + steps: - name: Connect to AWS uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 @@ -37,7 +37,7 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd with: terraform_version: "1.12.2" @@ -55,10 +55,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: 3.11 - cache: 'poetry' + cache: "poetry" - name: Unsubscribe MNS - working-directory: './lambdas/mns_subscription' + working-directory: "./lambdas/mns_subscription" env: SQS_ARN: ${{ env.ID_SYNC_QUEUE_ARN }} run: | diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 25c174ed9..a654d6ff4 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -12,8 +12,26 @@ env: LAMBDA_PATH: ${{ github.workspace }}/lambdas jobs: - lint: - name: Lint specification and Python projects + lint-specification: + name: Lint specification + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: "23.11.0" + cache: "npm" + + - name: Install linting dependencies + run: make install-node + + - name: Lint + run: make lint + + lint-python: + name: Lint Python projects runs-on: ubuntu-latest steps: @@ -22,23 +40,22 @@ jobs: - name: Install poetry run: pip install poetry==2.1.4 - # Base linting requires 3.8 due to APIM package dependencies. See root README for details under linting. - # Consider upgrading this and poetry deps if we move away from Azure DevOps to using the Proxygen tool. - uses: actions/setup-python@v6 with: - python-version: 3.8 - cache: 'poetry' - - - uses: actions/setup-node@v5 - with: - node-version: '23.11.0' - cache: 'npm' + python-version: 3.11 + cache: "poetry" - name: Install linting dependencies - run: make install-node && poetry install --no-root + run: poetry install --no-root + working-directory: quality_checks - name: Lint - run: make lint + run: poetry run make lint + working-directory: quality_checks + + - name: Check formatting + run: poetry run make format-check + working-directory: quality_checks testcoverage_and_sonarcloud: name: Test Coverage and SonarCloud @@ -53,7 +70,7 @@ jobs: - uses: actions/setup-python@v6 with: python-version: 3.11 - cache: 'poetry' + cache: "poetry" - name: Set up AWS credentials env: @@ -110,7 +127,7 @@ jobs: working-directory: delta_backend id: delta env: - PYTHONPATH: delta_backend/src:delta_backend/tests + PYTHONPATH: delta_backend/src:delta_backend/tests continue-on-error: true run: | poetry install @@ -144,9 +161,9 @@ jobs: PYTHONPATH: ${{ env.LAMBDA_PATH }}/ack_backend/src:${{ github.workspace }}/ack_backend/tests continue-on-error: true run: | - poetry install - poetry run coverage run --source=src -m unittest discover || echo "ack-lambda tests failed" >> ../../failed_tests.txt - poetry run coverage xml -o ../../ack-lambda-coverage.xml + poetry install + poetry run coverage run --source=src -m unittest discover || echo "ack-lambda tests failed" >> ../../failed_tests.txt + poetry run coverage xml -o ../../ack-lambda-coverage.xml - name: Run unittest with coverage-mns-subscription working-directory: lambdas/mns_subscription diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..2312dc587 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..d77752df6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +# This is a template, not a valid YAML file +/manifest_template.yml + +# We hit compile errors in Apigee if these are auto formatted +# TODO - investigate +/proxies/live/apiproxy/resources/jsc/ +/proxies/sandbox/apiproxy/resources/jsc/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 65cf5ffee..b16e98be4 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a6dab728..bdf0c7247 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,12 @@ # Contribution Guidelines ## Raising an Issue + If you raise an issue against this repository, please include as much information as possible to reproduce any bugs, or specific locations in the case of content errors. ## Contributing code + To contribute code, please fork the repository and raise a pull request. Ideally pull requests should be fairly granular and aim to solve one problem each. It would also be helpful if they @@ -12,11 +14,13 @@ linked to an issue. If the maintainers cannot understand why a pull request was so please explain why the changes need to be made (unless it is self-evident). ### Merge responsibility -* It is the responsibility of the reviewer to merge branches they have approved. -* It is the responsibility of the author of the merge to ensure their merge is in a mergeable state. -* It is the responsibility of the maintainers to ensure the merge process is unambiguous and automated where possible. + +- It is the responsibility of the reviewer to merge branches they have approved. +- It is the responsibility of the author of the merge to ensure their merge is in a mergeable state. +- It is the responsibility of the maintainers to ensure the merge process is unambiguous and automated where possible. ### Branch naming + Branch names should be of the format: `apm-nnn-short-issue-description` @@ -24,11 +28,12 @@ Branch names should be of the format: Multiple branches are permitted for the same ticket. ### Commit messages + Commit messages should be formatted as follows: + ``` APM-NNN Summary of changes Longer description of changes if explaining rationale is necessary, limited to 80 columns and spanning as many lines as you need. ``` - diff --git a/Makefile b/Makefile index 1247fec75..9114ccb37 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL=/usr/bin/env bash -euo pipefail PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS = backend batch_processor_filter delta_backend filenameprocessor mesh_processor recordprocessor lambdas/ack_backend lambdas/redis_sync lambdas/id_sync lambdas/mns_subscription lambdas/shared -PYTHON_PROJECT_DIRS = e2e e2e_batch $(PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS) +PYTHON_PROJECT_DIRS = e2e e2e_batch quality_checks $(PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS) #Installs dependencies using poetry. install-python: @@ -12,17 +12,12 @@ install-python: install-node: npm install --legacy-peer-deps -#Configures Git Hooks, which are scripts that run given a specified event. -.git/hooks/pre-commit: - cp scripts/pre-commit .git/hooks/pre-commit - #Condensed Target to run all targets above. -install: install-node install-python .git/hooks/pre-commit +install: install-node install-python #Run the npm linting script (specified in package.json). Used to check the syntax and formatting of files. lint: npm run lint - find . -name '*.py' -not -path '**/.venv/*' -not -path '**/.terraform/*'| xargs poetry run flake8 #Removes build/ + dist/ directories clean: diff --git a/README.md b/README.md index f3fbfee3e..2ea01cdc5 100644 --- a/README.md +++ b/README.md @@ -11,91 +11,102 @@ All other uses are as British English i.e. "immunisation". See https://nhsd-confluence.digital.nhs.uk/display/APM/Glossary. ## Directories + **Note:** Each Lambda has its own `README.md` file for detailed documentation. For non-Lambda-specific folders, refer to `README.specification.md`. ### Lambdas -| Folder | Description | -|---------------------|-------------| -| `backend` | **Imms API** – Handles CRUD operations for the Immunisation API. | -| `delta_backend` | **Imms Sync** – Lambda function that reacts to events in the Immunisation database. | +| Folder | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `backend` | **Imms API** – Handles CRUD operations for the Immunisation API. | +| `delta_backend` | **Imms Sync** – Lambda function that reacts to events in the Immunisation database. | | `ack_backend` | **Imms Batch** – Generates the final Business Acknowledgment (BUSACK) file from processed messages and writes it to the designated S3 location. | -| `filenameprocessor` | **Imms Batch** – Processes batch file names. | -| `mesh_processor` | **Imms Batch** – MESH-specific batch processing functionality. | -| `recordprocessor` | **Imms Batch** – Handles batch record processing. | -| `redis_sync` | **Imms Redis** – Handles sync s3 to REDIS. | -| `id_sync` | **Imms Redis** – Handles sync SQS to IEDS. | -| `shared` | **Imms Redis** – Not a lambda but Shared Code for lambdas | +| `filenameprocessor` | **Imms Batch** – Processes batch file names. | +| `mesh_processor` | **Imms Batch** – MESH-specific batch processing functionality. | +| `recordprocessor` | **Imms Batch** – Handles batch record processing. | +| `redis_sync` | **Imms Redis** – Handles sync s3 to REDIS. | +| `id_sync` | **Imms Redis** – Handles sync SQS to IEDS. | +| `shared` | **Imms Redis** – Not a lambda but Shared Code for lambdas | + --- ### Pipelines -| Folder | Description | -|---------|-------------| +| Folder | Description | +| ------- | ------------------------------------------- | | `azure` | Pipeline definition and orchestration code. | --- ### Infrastructure -| Folder | Description | -|------------------------|-------------| -| `infra` | Base infrastructure components. | -| `grafana` | Terraform configuration for Grafana, built on top of core infra. | -| `terraform` | Core Terraform infrastructure code. This is run in each PR and sets up lambdas associated with the PR.| -| `terraform_sandbox` | Sandbox environment for testing infrastructure changes. | -| `terraform_aws_backup` | Streamlined backup processing with AWS. | -| `proxies` | Apigee API proxy definitions. | +| Folder | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | +| `infra` | Base infrastructure components. | +| `grafana` | Terraform configuration for Grafana, built on top of core infra. | +| `terraform` | Core Terraform infrastructure code. This is run in each PR and sets up lambdas associated with the PR. | +| `terraform_sandbox` | Sandbox environment for testing infrastructure changes. | +| `terraform_aws_backup` | Streamlined backup processing with AWS. | +| `proxies` | Apigee API proxy definitions. | + --- ### Tests -| Folder | Description | -|---------------|-------------| -| `e2e` | End-to-end tests executed during PR pipelines. | -| `e2e_batch` | E2E tests specifically for batch-related functionality, also run in the PR pipeline. | -| `tests` | Sample e2e test. | +| Folder | Description | +| ----------- | ------------------------------------------------------------------------------------ | +| `e2e` | End-to-end tests executed during PR pipelines. | +| `e2e_batch` | E2E tests specifically for batch-related functionality, also run in the PR pipeline. | +| `tests` | Sample e2e test. | + --- ### Utilities -| Folder | Description | -|----------------|-------------| -| `devtools` | Helper tools and utilities for local development | +| Folder | Description | +| --------------- | ------------------------------------------------------------- | +| `devtools` | Helper tools and utilities for local development | | `scripts` | Standalone or reusable scripts for development and automation | -| `specification` | Specification files to document API and related definitions | -| `sandbox` | Simple sandbox API | +| `specification` | Specification files to document API and related definitions | +| `sandbox` | Simple sandbox API | --- ## Background: Python Package Management and Virtual Environments + - `pyenv` manages multiple Python versions at the system level, allowing you to install and switch between different Python versions for different projects. - `direnv` automates the loading of environment variables and can auto-activate virtual environments (.venv) when entering a project directory, making workflows smoother. - `.venv` (created via python -m venv or poetry) is Python’s built-in tool for isolating dependencies per project, ensuring that packages don’t interfere with global Python packages. -- `Poetry` is an all-in-one dependency and virtual environment manager that automatically creates a virtual environment (.venv), manages package installations, and locks dependencies (poetry.lock) for reproducibility, making it superior to using pip manually and it is used in all the lambda projects. +- `Poetry` is an all-in-one dependency and virtual environment manager that automatically creates a virtual environment (.venv), manages package installations, and locks dependencies (poetry.lock) for reproducibility, making it superior to using pip manually and it is used in all the lambda projects. + +## Project structure -## Project structure -To support a modular and maintainable architecture, each Lambda function in this project is structured as a self-contained folder with its own dependencies, configuration, and environment. +To support a modular and maintainable architecture, each Lambda function in this project is structured as a self-contained folder with its own dependencies, configuration, and environment. -We use Poetry to manage dependencies and virtual environments, with the virtualenvs.in-project setting enabled to ensure each Lambda has an isolated `.venv` created within its folder. +We use Poetry to manage dependencies and virtual environments, with the virtualenvs.in-project setting enabled to ensure each Lambda has an isolated `.venv` created within its folder. -Additionally, direnv is used alongside `.envrc` and `.env` files to automatically activate the appropriate virtual environment and load environment-specific variables when entering a folder. +Additionally, direnv is used alongside `.envrc` and `.env` files to automatically activate the appropriate virtual environment and load environment-specific variables when entering a folder. Each Lambda folder includes its own `.env` file for Lambda-specific settings, while the project root contains a separate `.env` and `.venv` for managing shared tooling, scripts, or infrastructure-related configurations. This setup promotes clear separation of concerns, reproducibility across environments, and simplifies local development, testing, and packaging for deployment. ## Environment setup + These dependencies are required for running and debugging the Lambda functions and end-to-end (E2E) tests. -### Install dependencies -Steps: +### Install dependencies + +Steps: + 1. Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) if running on Windows and install [Docker](https://docs.docker.com/engine/install/). 2. Install the following tools inside WSL. These will be used by the lambda and infrastructure code: + - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) - [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) 3. Open VS Code and click the bottom-left corner (blue section), then select **"Connect to WSL"** and choose your WSL distro (e.g., `Ubuntu-24.04`). -Once connected, you should see the path as something similar to: `/mnt/d/Source/immunisation-fhir-api/backend`. + Once connected, you should see the path as something similar to: `/mnt/d/Source/immunisation-fhir-api/backend`. 4. Run the following commands to install dependencies + ``` sudo apt update && sudo apt upgrade -y sudo apt install -y make build-essential libssl-dev zlib1g-dev \ @@ -106,35 +117,42 @@ Once connected, you should see the path as something similar to: `/mnt/d/Source/ ``` 5. Configure pyenv. + ``` pyenv install --list | grep "3.11" pyenv install 3.11.13 #current latest ``` 6. Install direnv if not already present, and hook it to the shell. + ``` sudo apt-get update && sudo apt-get install direnv echo 'eval "$(direnv hook bash)"' >> ~/.bashrc ``` -7. Install poetry +7. Install poetry ``` pip install poetry ``` ### Setting up a virtual environment with poetry + The steps below must be performed in each Lambda function folder and e2e folder to ensure the environment is correctly configured. For detailed instructions on running individual Lambdas, refer to the README.md files located inside each respective Lambda folder. -Steps: +Steps: + 1. Set the python version in the folder with the code used by lambda for example `./backend` (see [lambdas](#lambdas)) folder. + ``` pyenv local 3.11.13 # Set version in backend (this creates a .python-version file) ``` - Note: consult the lambda's `pyproject.toml` file to get the required Python version for this lambda. At the time of writing, this is `~3.10` for the batch lambdas and `~3.11` for all the others. + + Note: consult the lambda's `pyproject.toml` file to get the required Python version for this lambda. At the time of writing, this is `~3.10` for the batch lambdas and `~3.11` for all the others. 2. Configure poetry + ``` ### Point poetry virtual environment to .venv poetry config virtualenvs.in-project true @@ -143,16 +161,20 @@ Steps: ``` 3. Create an .env file and add environment variables. + ``` AWS_PROFILE={your_profile} IMMUNIZATION_ENV=local ``` + For unit tests to run successfully, you may also need to add an environment variable for PYTHONPATH. This should be: + ``` PYTHONPATH=src:tests ``` 4. Configure `direnv` by creating a `.envrc` file in the backend folder. This points direnv to the `.venv` created by poetry and loads env variables specified in the `.env` file + ``` export VIRTUAL_ENV=".venv" PATH_add "$VIRTUAL_ENV/bin" @@ -160,7 +182,7 @@ Steps: dotenv ``` -5. Restart bash and run `direnv allow`. You should see something similar like: +5. Restart bash and run `direnv allow`. You should see something similar like: ``` direnv: loading /mnt/d/Source/immunisation-fhir-api/.envrc direnv: export +AWS_PROFILE +IMMUNIZATION_ENV +VIRTUAL_ENV ~PATH @@ -172,16 +194,19 @@ Steps: It is not necessary to activate the virtual environment (using `source .venv/bin/activate`) before running a unit test suite from the command line; `direnv` will pick up the correct configurations for us. Run `pip list` to verify that the expected packages are installed. You should for example see that `recordprocessor` is specifically running `moto` v4, regardless of which if any `.venv` is active. ### Setting up the root level environment + The root-level virtual environment is primarily used for linting, as we create separate virtual environments for each folder that contains Lambda functions. -Steps: -1. Follow instructions above to [install dependencies](#install-dependencies) & [set up a virtual environment](#setting-up-a-virtual-environment-with-poetry). -**Note: While this project uses Python 3.11 (e.g. for Lambdas), the NHSDigital/api-management-utils repository — which orchestrates setup and linting — defaults to Python 3.8. -The linting command is executed from within that repo but calls the Makefile in this project, so be aware of potential Python version mismatches when running or debugging locally or in the pipeline.** +Steps: + +1. Follow instructions above to [install dependencies](#install-dependencies) & [set up a virtual environment](#setting-up-a-virtual-environment-with-poetry). + **Note: While this project uses Python 3.11 (e.g. for Lambdas), the NHSDigital/api-management-utils repository — which orchestrates setup and linting — defaults to Python 3.8. + The linting command is executed from within that repo but calls the Makefile in this project, so be aware of potential Python version mismatches when running or debugging locally or in the pipeline.** 2. Run `make lint`. This will: - Check the linting of the API specification yaml. - Run Flake8 on all Python files in the repository, excluding files inside .venv and .terraform directories. -## IDE setup +## IDE setup + The current team uses VS Code mainly. So this setup is targeted towards VS code. If you use another IDE please add the documentation to set up workspaces here. ### VS Code @@ -210,13 +235,14 @@ Note that unit tests can be run from the command line without VSCode configurati In order that VSCode can resolve modules in unit tests, it needs the PYTHONPATH. This should be setup in `backend/.vscode/launch.json` (see above). **NOTE:** In order to run unit test suites, you may need to manually switch to the correct virtual environment each time you wish to -run a different set of tests. To do this: +run a different set of tests. To do this: + - Show and Run Commands (Ctrl-Shift-P on Windows) - Python: Create Environment - Venv - Select the `.venv` named for the test suite you wish to run, e.g. `backend` - Use Existing -- VSCode should now display a toast saying that the following environment is selected: +- VSCode should now display a toast saying that the following environment is selected: - (e.g.) `/mnt/d/Source/immunisation-fhir-api/backend/.venv/bin/python` ### IntelliJ @@ -231,13 +257,12 @@ run a different set of tests. To do this: - Add the `src` and `tests` directories as sources. - Add `.direnv` as an exclusion if it's not already. - ## Verified commits -Please note that this project requires that all commits are verified using a GPG key. + +Please note that this project requires that all commits are verified using a GPG key. To set up a GPG key please follow the instructions specified here: https://docs.github.com/en/authentication/managing-commit-signature-verification - ## AWS configuration: Getting credentials for AWS federated user account In the 'Access keys' popup menu under AWS Access Portal: diff --git a/README.specification.md b/README.specification.md index e5dfb7bd2..dafe7d36f 100644 --- a/README.specification.md +++ b/README.specification.md @@ -2,19 +2,21 @@ ![Build](https://github.com/NHSDigital/immunisation-fhir-api/workflows/Build/badge.svg?branch=master) -This is a specification for the *immunisation-fhir-api* API. +This is a specification for the _immunisation-fhir-api_ API. -* `specification/` This [Open API Specification](https://swagger.io/docs/specification/about/) describes the endpoints, methods and messages exchanged by the API. Use it to generate interactive documentation; the contract between the API and its consumers. -* `sandbox/` This NodeJS application implements a mock implementation of the service. Use it as a back-end service to the interactive documentation to illustrate interactions and concepts. It is not intended to provide an exhaustive/faithful environment suitable for full development and testing. -* `scripts/` Utilities helpful to developers of this specification. -* `proxies/` Live (connecting to another service) and sandbox (using the sandbox container) Apigee API Proxy definitions. +- `specification/` This [Open API Specification](https://swagger.io/docs/specification/about/) describes the endpoints, methods and messages exchanged by the API. Use it to generate interactive documentation; the contract between the API and its consumers. +- `sandbox/` This NodeJS application implements a mock implementation of the service. Use it as a back-end service to the interactive documentation to illustrate interactions and concepts. It is not intended to provide an exhaustive/faithful environment suitable for full development and testing. +- `scripts/` Utilities helpful to developers of this specification. +- `proxies/` Live (connecting to another service) and sandbox (using the sandbox container) Apigee API Proxy definitions. Consumers of the API will find developer documentation on the [NHS Digital Developer Hub](https://digital.nhs.uk/developer). ## Contributing + Contributions to this project are welcome from anyone, providing that they conform to the [guidelines for contribution](https://github.com/NHSDigital/immunisation-fhir-api/blob/master/CONTRIBUTING.md) and the [community code of conduct](https://github.com/NHSDigital/immunisation-fhir-api/blob/master/CODE_OF_CONDUCT.md). ### Licensing + This code is dual licensed under the MIT license and the OGL (Open Government License). Any new work added to this repository must conform to the conditions of these licenses. In particular this means that this project may not depend on GPL-licensed or AGPL-licensed libraries, as these would violate the terms of those libraries' licenses. The contents of this repository are protected by Crown Copyright (C). @@ -22,17 +24,20 @@ The contents of this repository are protected by Crown Copyright (C). ## Development ### Requirements -* make -* nodejs + npm/yarn -* [poetry](https://github.com/python-poetry/poetry) -* Java 8+ + +- make +- nodejs + npm/yarn +- [poetry](https://github.com/python-poetry/poetry) +- Java 8+ ### Install + ``` $ make install ``` #### Updating hooks + You can install some pre-commit hooks to ensure you can't commit invalid spec changes by accident. These are also run in CI, but it's useful to run them locally too. @@ -41,60 +46,66 @@ $ make install-hooks ``` ### Environment Variables + Various scripts and commands rely on environment variables being set. These are documented with the commands. :bulb: Consider using [direnv](https://direnv.net/) to manage your environment variables during development and maintaining your own `.envrc` file - the values of these variables will be specific to you and/or sensitive. ### Make commands + There are `make` commands that alias some of this functionality: - * `lint` -- Lints the spec and code - * `publish` -- Outputs the specification as a **single file** into the `build/` directory - * `serve` -- Serves a preview of the specification in human-readable format + +- `lint` -- Lints the spec and code +- `publish` -- Outputs the specification as a **single file** into the `build/` directory +- `serve` -- Serves a preview of the specification in human-readable format ### Testing + Each API and team is unique. We encourage you to use a `test/` folder in the root of the project, and use whatever testing frameworks or apps your team feels comfortable with. It is important that the URL your test points to be configurable. We have included some stubs in the Makefile for running tests. ### VS Code Plugins - * [openapi-lint](https://marketplace.visualstudio.com/items?itemName=mermade.openapi-lint) resolves links and validates entire spec with the 'OpenAPI Resolve and Validate' command - * [OpenAPI (Swagger) Editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi) provides sidebar navigation - +- [openapi-lint](https://marketplace.visualstudio.com/items?itemName=mermade.openapi-lint) resolves links and validates entire spec with the 'OpenAPI Resolve and Validate' command +- [OpenAPI (Swagger) Editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi) provides sidebar navigation ### Emacs Plugins - * [**openapi-yaml-mode**](https://github.com/esc-emacs/openapi-yaml-mode) provides syntax highlighting, completion, and path help +- [**openapi-yaml-mode**](https://github.com/esc-emacs/openapi-yaml-mode) provides syntax highlighting, completion, and path help ### Speccy -> [Speccy](http://speccy.io/) *A handy toolkit for OpenAPI, with a linter to enforce quality rules, documentation rendering, and resolution.* +> [Speccy](http://speccy.io/) _A handy toolkit for OpenAPI, with a linter to enforce quality rules, documentation rendering, and resolution._ Speccy does the lifting for the following npm scripts: - * `test` -- Lints the definition - * `publish` -- Outputs the specification as a **single file** into the `build/` directory - * `serve` -- Serves a preview of the specification in human-readable format +- `test` -- Lints the definition +- `publish` -- Outputs the specification as a **single file** into the `build/` directory +- `serve` -- Serves a preview of the specification in human-readable format -(Workflow detailed in a [post](https://developerjack.com/blog/2018/maintaining-large-design-first-api-specs/) on the *developerjack* blog.) +(Workflow detailed in a [post](https://developerjack.com/blog/2018/maintaining-large-design-first-api-specs/) on the _developerjack_ blog.) :bulb: The `publish` command is useful when uploading to Apigee which requires the spec as a single file. ### Caveats #### Swagger UI + Swagger UI unfortunately doesn't correctly render `$ref`s in examples, so use `speccy serve` instead. #### Apigee Portal + The Apigee portal will not automatically pull examples from schemas, you must specify them manually. ### Platform setup As currently defined in your `proxies` folder, your proxies do pretty much nothing. -Telling Apigee how to connect to your backend requires a *Target Server*, which you should call named `immunisation-fhir-api-target`. -Our *Target Servers* defined in the [api-management-infrastructure](https://github.com/NHSDigital/api-management-infrastructure) repository. +Telling Apigee how to connect to your backend requires a _Target Server_, which you should call named `immunisation-fhir-api-target`. +Our _Target Servers_ defined in the [api-management-infrastructure](https://github.com/NHSDigital/api-management-infrastructure) repository. :bulb: For Sandbox-running environments (`test`) these need to be present for successful deployment but can be set to empty/dummy values. ### Detailed folder walk through + To get started developing your API use this template repo alongside guidance provided by the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Deliver+your+API) #### `/.github`: @@ -102,13 +113,15 @@ To get started developing your API use this template repo alongside guidance pro This folder contains templates that can be customised for items such as opening pull requests or issues within the repo `/.github/workflows`: This folder contains templates for github action workflows such as: + - `pr-jira-link.yaml`: This workflow template links Pull Requests to Jira tickets and runs when a pull request is opened. - `create-release-tag.yml`: This workflow template shows how to publish a Github release when pushing to master. #### `/azure`: Contains templates defining Azure Devops pipelines. By default the following pipelines are available: -- `azure-build-pipeline.yml`: Assembles the contents of your repository into a single file ("artifact") on Azure Devops and pushes any containers to our Docker registry. By default this pipeline is enabled for all branches. + +- `azure-build-pipeline.yml`: Assembles the contents of your repository into a single file ("artifact") on Azure Devops and pushes any containers to our Docker registry. By default this pipeline is enabled for all branches. - `azure-pr-pipeline.yml`: Deploys ephemeral versions of your proxy/spec to Apigee (and docker containers on AWS) to internal environments. You can run automated and manual tests against these while you develop. By default this pipeline will deploy to internal-dev, but the template can be amended to add other environments as required. - `azure-release-pipeline.yml`: Deploys the long-lived version of your pipeline to internal and external environments, typically when you merge to master. @@ -124,8 +137,8 @@ There are 2 folders `/live` and `/sandbox` allowing you to define a different pr Within the `live/apiproxy` and `sandbox/apiproxy` folders are: -`/proxies/default.xml`: Defines the proxy's Flows. Flows define how the proxy should handle different requests. By default, _ping and _status endpoint flows are defined. -See the APM confluence for more information on how the [_ping](https://nhsd-confluence.digital.nhs.uk/display/APM/_ping+endpoint) and [_status](https://nhsd-confluence.digital.nhs.uk/display/APM/_status+endpoint) endpoints work. +`/proxies/default.xml`: Defines the proxy's Flows. Flows define how the proxy should handle different requests. By default, \_ping and \_status endpoint flows are defined. +See the APM confluence for more information on how the [\_ping](https://nhsd-confluence.digital.nhs.uk/display/APM/_ping+endpoint) and [\_status](https://nhsd-confluence.digital.nhs.uk/display/APM/_status+endpoint) endpoints work. `/policies`: Populated with a set of standard XML Apigee policies that can be used in flows. @@ -135,8 +148,8 @@ See the APM confluence for more information on how the [_ping](https://nhsd-conf #### `/sandbox`: -This folder contains a template for a sandbox API. This example is a NodeJs application running in Docker. The application handles a few simple endpoints such as: /_ping, /health, /_status, /hello and some logging logic. -For more information about building sandbox APIs see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Setting+up+your+API+sandbox ). +This folder contains a template for a sandbox API. This example is a NodeJs application running in Docker. The application handles a few simple endpoints such as: /\_ping, /health, /\_status, /hello and some logging logic. +For more information about building sandbox APIs see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Setting+up+your+API+sandbox). #### `/scripts`: @@ -148,9 +161,10 @@ Create an OpenAPI Specification to document your API. For more information about #### `/tests`: -End to End tests. These tests are written in Python and use the PyTest test runner. Before running these tests you will need to set environment variables. The `test_endpoint.py` file provides a template of how to set up tests which test your api endpoints. For more information about testing your API see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Testing+your+API ). +End to End tests. These tests are written in Python and use the PyTest test runner. Before running these tests you will need to set environment variables. The `test_endpoint.py` file provides a template of how to set up tests which test your api endpoints. For more information about testing your API see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Testing+your+API). #### `Makefile`: + Useful make targets to get started including: installing dependencies and running smoke tests. #### `ecs-proxies-containers.yml ` and `ecs-proxies-deploy.yml`: @@ -161,11 +175,11 @@ These files are required to deploy containers alongside your Apigee proxy during `ecs-proxies-deploy.yml` : Here you can define config for your container deployment.   -For more information about deploying ECS containers see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Developing+ECS+proxies#DevelopingECSproxies-Buildingandpushingdockercontainers ). +For more information about deploying ECS containers see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Developing+ECS+proxies#DevelopingECSproxies-Buildingandpushingdockercontainers). #### `manifest_template.yml`: -This file defines 2 dictionaries of fields that are required for the Apigee deployment. For more info see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Manifest.yml+reference ). +This file defines 2 dictionaries of fields that are required for the Apigee deployment. For more info see the [API Producer Zone confluence](https://nhsd-confluence.digital.nhs.uk/display/APM/Manifest.yml+reference). #### Package management: diff --git a/SECURITY.md b/SECURITY.md index fb74718d6..7de9d497a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,23 +7,28 @@ private data managed by our systems, please do not hesitate to contact us using the methods outlined below. ## Reporting a vulnerability + **PLEASE NOTE: Email and HackerOne are our preferred methods of receiving reports.** ### Email + If you wish to notify us of a vulnerability via email, please include detailed information on the nature of the vulnerability and any steps required to reproduce it. You can reach us at: -* cybersecurity@nhs.net -* api.management@nhs.net + +- cybersecurity@nhs.net +- api.management@nhs.net ### HackerOne + If you are registered with HackerOne and have been admitted to the NHS Programme, you can report directly to us at: https://hackerone.com/nhs ### NCSC + You can send your report to the National Cyber Security Centre, who will assess your report and pass it on to NHS Digital if necessary. @@ -31,8 +36,10 @@ You can report vulnerabilities here: https://www.ncsc.gov.uk/information/vulnerability-reporting ### OpenBugBounty + We also accept bug reports via OpenBugBounty: https://www.openbugbounty.org/ ## General Security Enquiries + If you have general enquiries regarding our cyber security, please reach out to us at cybersecurity@nhs.net diff --git a/azure/azure-build-pipeline.yml b/azure/azure-build-pipeline.yml index cc49352fa..fbd3aa940 100644 --- a/azure/azure-build-pipeline.yml +++ b/azure/azure-build-pipeline.yml @@ -1,7 +1,7 @@ name: "$(SourceBranchName)+$(BuildID)" pool: - name: 'AWS-ECS' + name: "AWS-ECS" trigger: branches: @@ -13,7 +13,7 @@ trigger: pr: branches: - include: ['*'] + include: ["*"] resources: repositories: @@ -31,4 +31,4 @@ extends: service_name: ${{ variables.service_name }} short_service_name: ${{ variables.short_service_name }} post_ecs_push: - - template: ./templates/build.yml + - template: ./templates/build.yml diff --git a/azure/azure-pr-pipeline.yml b/azure/azure-pr-pipeline.yml index ed723e509..5d9c74855 100644 --- a/azure/azure-pr-pipeline.yml +++ b/azure/azure-pr-pipeline.yml @@ -11,17 +11,17 @@ resources: ref: refs/heads/edge endpoint: NHSDigital pipelines: - - pipeline: build_pipeline - source: "Immunisation-Fhir-Api-Build" - trigger: - branches: - exclude: - - master - - refs/heads/master - - refs/tags/v* + - pipeline: build_pipeline + source: "Immunisation-Fhir-Api-Build" + trigger: + branches: + exclude: + - master + - refs/heads/master + - refs/tags/v* pool: - name: 'AWS-ECS' + name: "AWS-ECS" variables: - template: project.yml @@ -39,7 +39,7 @@ extends: post_deploy: - template: ./templates/post-deploy.yml parameters: - aws_account_type: 'dev' + aws_account_type: "dev" - environment: internal-dev-sandbox proxy_path: sandbox jinja_templates: @@ -47,5 +47,5 @@ extends: post_deploy: - template: ./templates/post-deploy.yml parameters: - aws_account_type: 'dev' + aws_account_type: "dev" subscribe_to_mns: false diff --git a/azure/azure-prod-proxy-release-pipeline.yml b/azure/azure-prod-proxy-release-pipeline.yml index 60ae911b9..bacf46bfc 100644 --- a/azure/azure-prod-proxy-release-pipeline.yml +++ b/azure/azure-prod-proxy-release-pipeline.yml @@ -11,11 +11,11 @@ resources: ref: refs/heads/edge endpoint: NHSDigital pipelines: - - pipeline: build_pipeline - source: "Immunisation-Fhir-Api-Build" + - pipeline: build_pipeline + source: "Immunisation-Fhir-Api-Build" pool: - name: 'AWS-ECS' + name: "AWS-ECS" variables: - template: project.yml @@ -26,8 +26,8 @@ parameters: type: string default: blue values: - - blue - - green + - blue + - green extends: template: azure/common/apigee-deployment.yml@common diff --git a/azure/azure-prod-release-pipeline.yml b/azure/azure-prod-release-pipeline.yml index 0f478055f..ffb60b506 100644 --- a/azure/azure-prod-release-pipeline.yml +++ b/azure/azure-prod-release-pipeline.yml @@ -11,15 +11,15 @@ resources: ref: refs/heads/edge endpoint: NHSDigital pipelines: - - pipeline: build_pipeline - source: "Immunisation-Fhir-Api-Build" - trigger: - branches: - include: - - refs/tags/v* + - pipeline: build_pipeline + source: "Immunisation-Fhir-Api-Build" + trigger: + branches: + include: + - refs/tags/v* pool: - name: 'AWS-ECS' + name: "AWS-ECS" variables: - template: project.yml @@ -27,5 +27,5 @@ variables: extends: template: ./templates/deploy-stages.yml parameters: - service_name: ${{ variables.service_name }} + service_name: ${{ variables.service_name }} short_service_name: ${{ variables.short_service_name }} diff --git a/azure/azure-release-pipeline.yml b/azure/azure-release-pipeline.yml index 85f34220a..421bb8256 100644 --- a/azure/azure-release-pipeline.yml +++ b/azure/azure-release-pipeline.yml @@ -11,15 +11,15 @@ resources: ref: refs/heads/edge endpoint: NHSDigital pipelines: - - pipeline: build_pipeline - source: "Immunisation-Fhir-Api-Build" - trigger: - branches: - include: - - refs/tags/v* + - pipeline: build_pipeline + source: "Immunisation-Fhir-Api-Build" + trigger: + branches: + include: + - refs/tags/v* pool: - name: 'AWS-ECS' + name: "AWS-ECS" variables: - template: project.yml @@ -37,7 +37,7 @@ extends: post_deploy: - template: ./templates/post-deploy.yml parameters: - aws_account_type: 'dev' + aws_account_type: "dev" - environment: internal-dev-sandbox proxy_path: sandbox jinja_templates: @@ -45,7 +45,7 @@ extends: post_deploy: - template: ./templates/post-deploy.yml parameters: - aws_account_type: 'dev' + aws_account_type: "dev" subscribe_to_mns: false - environment: sandbox proxy_path: sandbox @@ -55,20 +55,20 @@ extends: - internal_dev_sandbox - environment: ref depends_on: - - internal_dev - - internal_dev_sandbox + - internal_dev + - internal_dev_sandbox jinja_templates: - DOMAIN_ENDPOINT: https://ref.imms.dev.vds.platform.nhs.uk + DOMAIN_ENDPOINT: https://ref.imms.dev.vds.platform.nhs.uk post_deploy: - - template: ./templates/post-deploy.yml - parameters: - aws_account_type: 'dev' + - template: ./templates/post-deploy.yml + parameters: + aws_account_type: "dev" - environment: int depends_on: - internal_dev jinja_templates: DOMAIN_ENDPOINT: https://int.imms.dev.vds.platform.nhs.uk post_deploy: - - template: ./templates/post-deploy.yml - parameters: - aws_account_type: 'dev' + - template: ./templates/post-deploy.yml + parameters: + aws_account_type: "dev" diff --git a/azure/project.yml b/azure/project.yml index 5e7fbfda4..8b28559f6 100644 --- a/azure/project.yml +++ b/azure/project.yml @@ -8,4 +8,3 @@ variables: value: immunisation-fhir-api/FHIR/R4 - name: pr_number value: ${{ split(variables['Build.SourceBranch'], '/')[2] }} - diff --git a/azure/templates/aws-assume-role.yml b/azure/templates/aws-assume-role.yml index fed99a3c2..53cf3b711 100644 --- a/azure/templates/aws-assume-role.yml +++ b/azure/templates/aws-assume-role.yml @@ -1,71 +1,71 @@ parameters: - - name: 'role' + - name: "role" type: string - - name: 'profile' + - name: "profile" type: string - default: '' - - name: 'aws_account' + default: "" + - name: "aws_account" type: string - default: 'ptl' + default: "ptl" steps: - - template: "azure/components/aws-clean-config.yml@common" + - template: "azure/components/aws-clean-config.yml@common" - - bash: | - set -e - echo "##vso[task.setvariable variable=ROLE]${{ parameters.role }}" - displayName: get imms role name - - bash: | - set -e - aws_role="$(ROLE)" - echo "assume role: '${aws_role}'" - if [[ "${{ parameters.aws_account }}" =~ ^(prod|dev)$ ]]; then - echo "account is ${{ parameters.aws_account }}" - account_id="$(aws ssm get-parameter --name /imms-account-ids/${{ parameters.aws_account }} --query Parameter.Value --output text)" - aws_role="arn:aws:iam::${account_id}:role/${aws_role}" - fi - if [[ "${aws_role}" != arn:aws:iam:* ]]; then - echo "check if role exists" - # iam synchronisation issues can take a few to make the role appear - for i in {1..15}; do - if aws iam get-role --role-name ${aws_role} > /dev/null; then - echo role exists - sleep 2 - break - fi - echo waiting for role ... + - bash: | + set -e + echo "##vso[task.setvariable variable=ROLE]${{ parameters.role }}" + displayName: get imms role name + - bash: | + set -e + aws_role="$(ROLE)" + echo "assume role: '${aws_role}'" + if [[ "${{ parameters.aws_account }}" =~ ^(prod|dev)$ ]]; then + echo "account is ${{ parameters.aws_account }}" + account_id="$(aws ssm get-parameter --name /imms-account-ids/${{ parameters.aws_account }} --query Parameter.Value --output text)" + aws_role="arn:aws:iam::${account_id}:role/${aws_role}" + fi + if [[ "${aws_role}" != arn:aws:iam:* ]]; then + echo "check if role exists" + # iam synchronisation issues can take a few to make the role appear + for i in {1..15}; do + if aws iam get-role --role-name ${aws_role} > /dev/null; then + echo role exists sleep 2 - done - account_id="$(aws sts get-caller-identity --query Account --output text)" - aws_role="arn:aws:iam::${account_id}:role/${aws_role}" - fi - cp ~/.aws/config.default ~/.aws/config - tmp_file="$(Agent.TempDirectory)/.aws.tmp.creds.json" - # add some backoff to allow for eventual consistency of IAM - for i in {2..4}; - do - if aws sts assume-role --role-arn "${aws_role}" --role-session-name build-assume-role > ${tmp_file}; then - echo assumed role - assumed_role="yes" - break + break fi - let "sleep_for=$i*10"; - sleep $sleep_for - done - if [[ "${assumed_role}" != "yes" ]]; then - echo "assume role failed" - exit -1 - fi - echo "aws_access_key_id = $(jq -r .Credentials.AccessKeyId ${tmp_file})" >> ~/.aws/config - echo "aws_secret_access_key = $(jq -r .Credentials.SecretAccessKey ${tmp_file})" >> ~/.aws/config - echo "aws_session_token = $(jq -r .Credentials.SessionToken ${tmp_file})" >> ~/.aws/config - expiry=$(jq -r .Credentials.Expiration ${tmp_file}) - echo "##vso[task.setvariable variable=ASSUME_ROLE_EXPIRY;]$expiry" - rm ${tmp_file} - profile="${{ parameters.profile }}" - if [[ ! -z "${profile}" ]]; then - echo as profile ${profile} - sed -i "s#\[default\]#\[profile ${profile}\]#" ~/.aws/config - fi - displayName: assume role - condition: and(succeeded(), ne(variables['ROLE'], '')) \ No newline at end of file + echo waiting for role ... + sleep 2 + done + account_id="$(aws sts get-caller-identity --query Account --output text)" + aws_role="arn:aws:iam::${account_id}:role/${aws_role}" + fi + cp ~/.aws/config.default ~/.aws/config + tmp_file="$(Agent.TempDirectory)/.aws.tmp.creds.json" + # add some backoff to allow for eventual consistency of IAM + for i in {2..4}; + do + if aws sts assume-role --role-arn "${aws_role}" --role-session-name build-assume-role > ${tmp_file}; then + echo assumed role + assumed_role="yes" + break + fi + let "sleep_for=$i*10"; + sleep $sleep_for + done + if [[ "${assumed_role}" != "yes" ]]; then + echo "assume role failed" + exit -1 + fi + echo "aws_access_key_id = $(jq -r .Credentials.AccessKeyId ${tmp_file})" >> ~/.aws/config + echo "aws_secret_access_key = $(jq -r .Credentials.SecretAccessKey ${tmp_file})" >> ~/.aws/config + echo "aws_session_token = $(jq -r .Credentials.SessionToken ${tmp_file})" >> ~/.aws/config + expiry=$(jq -r .Credentials.Expiration ${tmp_file}) + echo "##vso[task.setvariable variable=ASSUME_ROLE_EXPIRY;]$expiry" + rm ${tmp_file} + profile="${{ parameters.profile }}" + if [[ ! -z "${profile}" ]]; then + echo as profile ${profile} + sed -i "s#\[default\]#\[profile ${profile}\]#" ~/.aws/config + fi + displayName: assume role + condition: and(succeeded(), ne(variables['ROLE'], '')) diff --git a/azure/templates/deploy-manual-approval.yml b/azure/templates/deploy-manual-approval.yml index edaf132ea..4056814d8 100644 --- a/azure/templates/deploy-manual-approval.yml +++ b/azure/templates/deploy-manual-approval.yml @@ -1,13 +1,13 @@ -jobs: -- job: waitForValidation - displayName: Wait for external validation - pool: server - timeoutInMinutes: 60 # job times out in 60 mins - steps: - - task: ManualValidation@0 - timeoutInMinutes: 10 # task times out in 10 mins - inputs: - notifyUsers: | - VAAL1@hscic.gov.uk - instructions: 'Please validate the build configuration and approve' - onTimeout: 'approve' \ No newline at end of file +jobs: + - job: waitForValidation + displayName: Wait for external validation + pool: server + timeoutInMinutes: 60 # job times out in 60 mins + steps: + - task: ManualValidation@0 + timeoutInMinutes: 10 # task times out in 10 mins + inputs: + notifyUsers: | + VAAL1@hscic.gov.uk + instructions: "Please validate the build configuration and approve" + onTimeout: "approve" diff --git a/azure/templates/deploy-stage.yml b/azure/templates/deploy-stage.yml index ffcca932c..4a9e22375 100644 --- a/azure/templates/deploy-stage.yml +++ b/azure/templates/deploy-stage.yml @@ -5,7 +5,7 @@ parameters: type: string - name: fully_qualified_service_name type: string - default: '' + default: "" - name: environment type: string - name: variables @@ -13,10 +13,10 @@ parameters: default: [] - name: pr_label type: string - default: '' + default: "" - name: proxy_path type: string - default: 'internal-dev' + default: "internal-dev" - name: secret_file_ids type: object default: [] @@ -41,14 +41,13 @@ jobs: displayName: Deployment timeoutInMinutes: 30 pool: - name: 'AWS-ECS' + name: "AWS-ECS" workspace: clean: all variables: ${{ each var in parameters.variables }}: ${{ var.key }}: ${{ var.value }} steps: - - bash: | if [ ! -z "$(ls -A \"$(Pipeline.Workspace)/s/${{ parameters.service_name }}\" 2>/dev/null)" ]; then echo "workspace directory is not empty!" @@ -67,89 +66,89 @@ jobs: - template: azure/components/aws-clean-config.yml@common - ${{ if parameters.notify }}: - - template: azure/components/aws-assume-role.yml@common - parameters: - role: "auto-ops" - profile: "apm_ptl" + - template: azure/components/aws-assume-role.yml@common + parameters: + role: "auto-ops" + profile: "apm_ptl" - - template: azure/components/get-aws-secrets-and-ssm-params.yml@common - parameters: - secret_file_ids: - - ${{ each secret_file_id in parameters.secret_file_ids }}: - - ${{ secret_file_id }} - secret_ids: - - ptl/access-tokens/github/repo-status-update/GITHUB_ACCESS_TOKEN - - ${{ each secret_id in parameters.secret_ids }}: - - ${{ secret_id }} - config_ids: - - /ptl/azure-devops/GITHUB_USER - - ${{ each config_id in parameters.config_ids }}: - - ${{ config_id }} + - template: azure/components/get-aws-secrets-and-ssm-params.yml@common + parameters: + secret_file_ids: + - ${{ each secret_file_id in parameters.secret_file_ids }}: + - ${{ secret_file_id }} + secret_ids: + - ptl/access-tokens/github/repo-status-update/GITHUB_ACCESS_TOKEN + - ${{ each secret_id in parameters.secret_ids }}: + - ${{ secret_id }} + config_ids: + - /ptl/azure-devops/GITHUB_USER + - ${{ each config_id in parameters.config_ids }}: + - ${{ config_id }} - - bash: | - echo "Build.SourceBranch: $(Build.SourceBranch)" - echo "Build.SourceBranchName: $(Build.SourceBranchName)" - echo "Build.SourceVersion: $(Build.SourceVersion)" - echo "Build.SourceVersionMessage: $(Build.SourceVersionMessage)" - if [[ ! -z $(NOTIFY_COMMIT_SHA) ]]; then - echo "##[debug]Using already provided NOTIFY_COMMIT_SHA=$(NOTIFY_COMMIT_SHA)" - else - NOTIFY_COMMIT_SHA="" - if [[ "$(Build.SourceBranch)" =~ ^refs/tags/.+$ ]]; then - echo "##[debug]Build appears to be a tag build" - echo "##[debug]Using Build.SourceVersion as NOTIFY_COMMIT_SHA" - NOTIFY_COMMIT_SHA="$(Build.SourceVersion)" - fi - if [[ "$(Build.SourceBranch)" =~ ^refs/pull/.+$ ]]; then - echo "##[debug]Build appears to be a pull request build" - echo "##[debug]Extracting NOTIFY_COMMIT_SHA from Build.SourceVersionMessage" - NOTIFY_COMMIT_SHA=`echo "$(Build.SourceVersionMessage)" | cut -d' ' -f2` - fi - if [[ -z $NOTIFY_COMMIT_SHA ]]; then - echo "##[debug]Build does not appear to be pull or tag build" - echo "##[debug]Using Build.SourceVersion as NOTIFY_COMMIT_SHA" - NOTIFY_COMMIT_SHA="$(Build.SourceVersion)" - fi - echo "##vso[task.setvariable variable=NOTIFY_COMMIT_SHA]$NOTIFY_COMMIT_SHA" - fi - displayName: Set NOTIFY_COMMIT_SHA - condition: always() - - template: azure/components/update-github-status.yml@common - parameters: - state: pending - description: "Deployment started" + - bash: | + echo "Build.SourceBranch: $(Build.SourceBranch)" + echo "Build.SourceBranchName: $(Build.SourceBranchName)" + echo "Build.SourceVersion: $(Build.SourceVersion)" + echo "Build.SourceVersionMessage: $(Build.SourceVersionMessage)" + if [[ ! -z $(NOTIFY_COMMIT_SHA) ]]; then + echo "##[debug]Using already provided NOTIFY_COMMIT_SHA=$(NOTIFY_COMMIT_SHA)" + else + NOTIFY_COMMIT_SHA="" + if [[ "$(Build.SourceBranch)" =~ ^refs/tags/.+$ ]]; then + echo "##[debug]Build appears to be a tag build" + echo "##[debug]Using Build.SourceVersion as NOTIFY_COMMIT_SHA" + NOTIFY_COMMIT_SHA="$(Build.SourceVersion)" + fi + if [[ "$(Build.SourceBranch)" =~ ^refs/pull/.+$ ]]; then + echo "##[debug]Build appears to be a pull request build" + echo "##[debug]Extracting NOTIFY_COMMIT_SHA from Build.SourceVersionMessage" + NOTIFY_COMMIT_SHA=`echo "$(Build.SourceVersionMessage)" | cut -d' ' -f2` + fi + if [[ -z $NOTIFY_COMMIT_SHA ]]; then + echo "##[debug]Build does not appear to be pull or tag build" + echo "##[debug]Using Build.SourceVersion as NOTIFY_COMMIT_SHA" + NOTIFY_COMMIT_SHA="$(Build.SourceVersion)" + fi + echo "##vso[task.setvariable variable=NOTIFY_COMMIT_SHA]$NOTIFY_COMMIT_SHA" + fi + displayName: Set NOTIFY_COMMIT_SHA + condition: always() + - template: azure/components/update-github-status.yml@common + parameters: + state: pending + description: "Deployment started" - - bash: | - set -euo pipefail - echo "For backward compatibility..." - echo "##vso[task.setvariable variable=APIGEE_ENVIRONMENT]${{ parameters.environment }}" - displayName: Setting AWS_ENVIRONMENT=${{ parameters.environment }} - - bash: | - if [[ ! -z $(UTILS_PR_NUMBER) ]]; then - echo "##[debug]Triggered from utils repository, PR_NUMBER=$(UTILS_PR_NUMBER)" - echo "##vso[task.setvariable variable=PR_NUMBER]$(UTILS_PR_NUMBER)" - else - echo "##[debug]PR_NUMBER=pr-$(System.PullRequest.PullRequestNumber)" - echo "##vso[task.setvariable variable=PR_LABEL]pr-$(System.PullRequest.PullRequestNumber)" - fi - displayName: Set PR Label - - bash: | - set -euo pipefail - if [[ ! -z $(UTILS_PR_NUMBER) ]]; then - if [[ "${{ parameters.proxy_path }}" == "live" ]]; then - export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)" - echo "##vso[task.setvariable variable=FULLY_QUALIFIED_SERVICE_NAME]${FULLY_QUALIFIED_SERVICE_NAME}" + - bash: | + set -euo pipefail + echo "For backward compatibility..." + echo "##vso[task.setvariable variable=APIGEE_ENVIRONMENT]${{ parameters.environment }}" + displayName: Setting AWS_ENVIRONMENT=${{ parameters.environment }} + - bash: | + if [[ ! -z $(UTILS_PR_NUMBER) ]]; then + echo "##[debug]Triggered from utils repository, PR_NUMBER=$(UTILS_PR_NUMBER)" + echo "##vso[task.setvariable variable=PR_NUMBER]$(UTILS_PR_NUMBER)" + else + echo "##[debug]PR_NUMBER=pr-$(System.PullRequest.PullRequestNumber)" + echo "##vso[task.setvariable variable=PR_LABEL]pr-$(System.PullRequest.PullRequestNumber)" + fi + displayName: Set PR Label + - bash: | + set -euo pipefail + if [[ ! -z $(UTILS_PR_NUMBER) ]]; then + if [[ "${{ parameters.proxy_path }}" == "live" ]]; then + export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)" + echo "##vso[task.setvariable variable=FULLY_QUALIFIED_SERVICE_NAME]${FULLY_QUALIFIED_SERVICE_NAME}" + else + export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)-${{ parameters.proxy_path }}" + echo "##vso[task.setvariable variable=FULLY_QUALIFIED_SERVICE_NAME]${FULLY_QUALIFIED_SERVICE_NAME}" + fi + echo "##[debug]Triggered from utils repository, FULLY_QUALIFIED_SERVICE_NAME=${FULLY_QUALIFIED_SERVICE_NAME}" else - export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)-${{ parameters.proxy_path }}" + export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)" + echo "##[debug]FULLY_QUALIFIED_SERVICE_NAME=${{ parameters.service_name }}-$(PR_LABEL)" echo "##vso[task.setvariable variable=FULLY_QUALIFIED_SERVICE_NAME]${FULLY_QUALIFIED_SERVICE_NAME}" fi - echo "##[debug]Triggered from utils repository, FULLY_QUALIFIED_SERVICE_NAME=${FULLY_QUALIFIED_SERVICE_NAME}" - else - export FULLY_QUALIFIED_SERVICE_NAME="${{ parameters.service_name }}-$(PR_LABEL)" - echo "##[debug]FULLY_QUALIFIED_SERVICE_NAME=${{ parameters.service_name }}-$(PR_LABEL)" - echo "##vso[task.setvariable variable=FULLY_QUALIFIED_SERVICE_NAME]${FULLY_QUALIFIED_SERVICE_NAME}" - fi - displayName: Override FULLY_QUALIFIED_SERVICE_NAME + displayName: Override FULLY_QUALIFIED_SERVICE_NAME - checkout: self path: "s/${{ parameters.service_name }}" @@ -163,14 +162,14 @@ jobs: - ${{ post_init }} - ${{ if parameters.notify }}: - - template: azure/components/update-github-status.yml@common - parameters: - state: success - on_success: true - description: "Deploy succeeded" + - template: azure/components/update-github-status.yml@common + parameters: + state: success + on_success: true + description: "Deploy succeeded" - - template: azure/components/update-github-status.yml@common - parameters: - state: failure - on_failure: true - description: "Deploy failed" + - template: azure/components/update-github-status.yml@common + parameters: + state: failure + on_failure: true + description: "Deploy failed" diff --git a/azure/templates/deploy-stages.yml b/azure/templates/deploy-stages.yml index 2dc17f00c..54c0f4d83 100644 --- a/azure/templates/deploy-stages.yml +++ b/azure/templates/deploy-stages.yml @@ -1,10 +1,10 @@ parameters: - name: service_name type: string - default: 'immunisation-fhir-api' + default: "immunisation-fhir-api" - name: short_service_name type: string - default: 'ifa' + default: "ifa" stages: - stage: Prod_Green_Deployment_Approval @@ -20,10 +20,10 @@ stages: short_service_name: ${{ parameters.short_service_name }} environment: prod post_init: - - template: post-prod-deploy.yml - parameters: - aws_account_type: 'prod' - deployment_type: 'green' + - template: post-prod-deploy.yml + parameters: + aws_account_type: "prod" + deployment_type: "green" - stage: Prod_Blue_Deployment_Approval isSkippable: false dependsOn: [] @@ -38,7 +38,7 @@ stages: short_service_name: ${{ parameters.short_service_name }} environment: prod post_init: - - template: post-prod-deploy.yml - parameters: - aws_account_type: 'prod' - deployment_type: 'blue' \ No newline at end of file + - template: post-prod-deploy.yml + parameters: + aws_account_type: "prod" + deployment_type: "blue" diff --git a/azure/templates/post-deploy.yml b/azure/templates/post-deploy.yml index 44e0578fa..413d7818b 100644 --- a/azure/templates/post-deploy.yml +++ b/azure/templates/post-deploy.yml @@ -3,7 +3,7 @@ parameters: default: aws --profile=apim-dev - name: is_ptl default: true - - name: 'aws_account_type' + - name: "aws_account_type" type: string - name: subscribe_to_mns type: boolean @@ -11,22 +11,22 @@ parameters: steps: - ${{ if parameters.is_ptl }}: - - template: "azure/components/aws-assume-role.yml@common" - parameters: - role: "auto-ops" - profile: "apm_ptl" - - - template: "azure/components/get-aws-secrets-and-ssm-params.yml@common" - parameters: - secret_file_ids: - - ptl/app-credentials/jwt_testing/non-prod/JWT_TESTING_PRIVATE_KEY - secret_ids: - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_ID - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_SECRET - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_ID_INT - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_SECRET_INT - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INT_CLIENT_ID - - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INT_CLIENT_SECRET + - template: "azure/components/aws-assume-role.yml@common" + parameters: + role: "auto-ops" + profile: "apm_ptl" + + - template: "azure/components/get-aws-secrets-and-ssm-params.yml@common" + parameters: + secret_file_ids: + - ptl/app-credentials/jwt_testing/non-prod/JWT_TESTING_PRIVATE_KEY + secret_ids: + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_ID + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_SECRET + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_ID_INT + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INTROSPECTION_CLIENT_SECRET_INT + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INT_CLIENT_ID + - ptl/app-credentials/immunisation-fhir-api-testing-app/non-prod/INT_CLIENT_SECRET - bash: | make install-python @@ -81,21 +81,21 @@ steps: retryCountOnTaskFailure: 2 - ${{ if eq(parameters.subscribe_to_mns, true) }}: - - bash: | - export AWS_PROFILE=apim-dev - echo "Subscribing SQS to MNS for notifications." - pyenv install -s 3.11.11 - pyenv local 3.11.11 - echo "Setting up poetry environment..." - poetry env use 3.11 - poetry install --no-root - - echo "Subscribing SQS to MNS for notifications..." - make subscribe - displayName: "Run MNS Subscription" - workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/lambdas/mns_subscription" - env: - SQS_ARN: "$(ID_SYNC_QUEUE_ARN)" + - bash: | + export AWS_PROFILE=apim-dev + echo "Subscribing SQS to MNS for notifications." + pyenv install -s 3.11.11 + pyenv local 3.11.11 + echo "Setting up poetry environment..." + poetry env use 3.11 + poetry install --no-root + + echo "Subscribing SQS to MNS for notifications..." + make subscribe + displayName: "Run MNS Subscription" + workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/lambdas/mns_subscription" + env: + SQS_ARN: "$(ID_SYNC_QUEUE_ARN)" - bash: | set -ex @@ -227,8 +227,8 @@ steps: workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/e2e_batch" - task: PublishTestResults@2 - displayName: 'Publish test results' + displayName: "Publish test results" condition: always() inputs: - testResultsFiles: '$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/tests/test-report.xml' + testResultsFiles: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/tests/test-report.xml" failTaskOnFailedTests: true diff --git a/azure/templates/post-prod-deploy.yml b/azure/templates/post-prod-deploy.yml index 861ef3457..c303f78fd 100644 --- a/azure/templates/post-prod-deploy.yml +++ b/azure/templates/post-prod-deploy.yml @@ -3,9 +3,9 @@ parameters: default: aws --profile=apim-dev - name: is_ptl default: true - - name: 'aws_account_type' + - name: "aws_account_type" type: string - - name: 'deployment_type' + - name: "deployment_type" type: string steps: @@ -19,7 +19,7 @@ steps: pwd ls -la cd terraform - displayName: 'Check Directory and Navigate to Terraform for AWS Deployment' + displayName: "Check Directory and Navigate to Terraform for AWS Deployment" workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)" - bash: | diff --git a/azure/templates/run-tests.yml b/azure/templates/run-tests.yml index c32ef3416..b97d7ba14 100644 --- a/azure/templates/run-tests.yml +++ b/azure/templates/run-tests.yml @@ -4,10 +4,10 @@ parameters: default: false - name: test_command type: string - default: 'make test' + default: "make test" - name: smoketest_command type: string - default: 'make smoketest' + default: "make smoketest" steps: - bash: | @@ -17,38 +17,38 @@ steps: condition: always() - ${{ if parameters.full }}: - # In order to run tests in prod you must supply the unique ID of an Apigee app - # that has authorized access to your service proxy. - - bash: | - export PROXY_NAME="$(FULLY_QUALIFIED_SERVICE_NAME)" - export APIGEE_ACCESS_TOKEN="$(secret.AccessToken)" - export APIGEE_APP_ID="MY APP ID" - export STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" - export SOURCE_COMMIT_ID="$(Build.SourceVersion)" - ${{ parameters.test_command }} - workingDirectory: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME) - displayName: Run full test suite - - task: PublishTestResults@2 - displayName: 'Publish test results' - condition: always() - inputs: - testResultsFiles: '$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/test-report.xml' - failTaskOnFailedTests: true + # In order to run tests in prod you must supply the unique ID of an Apigee app + # that has authorized access to your service proxy. + - bash: | + export PROXY_NAME="$(FULLY_QUALIFIED_SERVICE_NAME)" + export APIGEE_ACCESS_TOKEN="$(secret.AccessToken)" + export APIGEE_APP_ID="MY APP ID" + export STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" + export SOURCE_COMMIT_ID="$(Build.SourceVersion)" + ${{ parameters.test_command }} + workingDirectory: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME) + displayName: Run full test suite + - task: PublishTestResults@2 + displayName: "Publish test results" + condition: always() + inputs: + testResultsFiles: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/test-report.xml" + failTaskOnFailedTests: true - ${{ if not(parameters.full) }}: - # In order to run tests in prod you must supply the unique ID of an Apigee app - # that has authorized access to your service proxy. - - bash: | - export PROXY_NAME="$(FULLY_QUALIFIED_SERVICE_NAME)" - export APIGEE_ACCESS_TOKEN="$(secret.AccessToken)" - export APIGEE_APP_ID="MY APP ID" - export STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" - export SOURCE_COMMIT_ID="$(Build.SourceVersion)" - ${{ parameters.smoketest_command }} - workingDirectory: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME) - displayName: Run smoketests - - task: PublishTestResults@2 - displayName: 'Publish smoketest results' - condition: always() - inputs: - testResultsFiles: '$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/smoketest-report.xml' - failTaskOnFailedTests: true + # In order to run tests in prod you must supply the unique ID of an Apigee app + # that has authorized access to your service proxy. + - bash: | + export PROXY_NAME="$(FULLY_QUALIFIED_SERVICE_NAME)" + export APIGEE_ACCESS_TOKEN="$(secret.AccessToken)" + export APIGEE_APP_ID="MY APP ID" + export STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" + export SOURCE_COMMIT_ID="$(Build.SourceVersion)" + ${{ parameters.smoketest_command }} + workingDirectory: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME) + displayName: Run smoketests + - task: PublishTestResults@2 + displayName: "Publish smoketest results" + condition: always() + inputs: + testResultsFiles: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/smoketest-report.xml" + failTaskOnFailedTests: true diff --git a/backend/README.md b/backend/README.md index 4509d214e..a49b3c4e7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,14 +1,16 @@ - # About + This document describes the environment setup for the backend API Lambda. This Lambda handles incoming CRUD operation requests from APIGEE and interacts with the immunisation events database to store immunisation records. All commands listed below are run in the `./backend` folder. ## Setting up the backend lambda + Note: Paths are relative to this directory, `backend`. 1. Follow the instructions in the root level README.md to setup the [dependencies](../README.md#environment-setup) and create a [virtual environment](../README.md#) for this folder. 2. Replace the `.env` file in the backend folder. Note the variables might change in the future. These environment variables will be loaded automatically when using `direnv`. + ``` AWS_PROFILE={your-profile} DYNAMODB_TABLE_NAME=imms-{environment}-imms-events diff --git a/backend/src/__init__.py b/backend/src/__init__.py index 8b1378917..e69de29bb 100644 --- a/backend/src/__init__.py +++ b/backend/src/__init__.py @@ -1 +0,0 @@ - diff --git a/backend/src/authorisation/authoriser.py b/backend/src/authorisation/authoriser.py index aaf9370b5..ac5e48026 100644 --- a/backend/src/authorisation/authoriser.py +++ b/backend/src/authorisation/authoriser.py @@ -1,4 +1,5 @@ """Authoriser class""" + import json from authorisation.api_operation_code import ApiOperationCode @@ -8,13 +9,17 @@ class Authoriser: """Authoriser class. Used for authorising operations on FHIR vaccinations.""" + def __init__(self): self._cache_client = redis_client @staticmethod - def _expand_permissions(permissions: list[str]) -> dict[str, list[ApiOperationCode]]: + def _expand_permissions( + permissions: list[str], + ) -> dict[str, list[ApiOperationCode]]: """Parses and expands permissions data into a dictionary mapping vaccination types to a list of permitted - API operations. The raw string from Redis will be in the form VAC.PERMS e.g. COVID19.CRUDS""" + API operations. The raw string from Redis will be in the form VAC.PERMS e.g. COVID19.CRUDS + """ expanded_permissions = {} for permission in permissions: @@ -39,7 +44,7 @@ def authorise( self, supplier_system: str, requested_operation: ApiOperationCode, - vaccination_types: set[str] + vaccination_types: set[str], ) -> bool: """Checks that the supplier system is permitted to carry out the requested operation on the given vaccination type(s)""" @@ -58,7 +63,7 @@ def filter_permitted_vacc_types( self, supplier_system: str, requested_operation: ApiOperationCode, - vaccination_types: set[str] + vaccination_types: set[str], ) -> set[str]: """Returns the set of vaccine types that a given supplier can interact with for a given operation type. This is a more permissive form of authorisation e.g. used in search as it will filter out any requested vacc diff --git a/backend/src/clients.py b/backend/src/clients.py index bae19749e..83b4a9569 100644 --- a/backend/src/clients.py +++ b/backend/src/clients.py @@ -1,9 +1,10 @@ """Initialise s3, kinesis, lambda and redis clients""" -from boto3 import client as boto3_client -import os import logging +import os + import redis +from boto3 import client as boto3_client REGION_NAME = os.getenv("AWS_REGION", "eu-west-2") diff --git a/backend/src/controller/aws_apig_event_utils.py b/backend/src/controller/aws_apig_event_utils.py index 63146694c..87b6bb05e 100644 --- a/backend/src/controller/aws_apig_event_utils.py +++ b/backend/src/controller/aws_apig_event_utils.py @@ -1,4 +1,5 @@ """Utility module for interacting with the AWS API Gateway event provided to controllers""" + from typing import Optional from aws_lambda_typing.events import APIGatewayProxyEventV1 @@ -9,11 +10,7 @@ def get_path_parameter(event: APIGatewayProxyEventV1, param_name: str) -> str: - return dict_utils.get_field( - event["pathParameters"], - param_name, - default="" - ) + return dict_utils.get_field(event["pathParameters"], param_name, default="") def get_supplier_system_header(event: APIGatewayProxyEventV1) -> str: diff --git a/backend/src/controller/aws_apig_response_utils.py b/backend/src/controller/aws_apig_response_utils.py index 858a799eb..01b42b61e 100644 --- a/backend/src/controller/aws_apig_response_utils.py +++ b/backend/src/controller/aws_apig_response_utils.py @@ -1,13 +1,10 @@ """Utility module providing helper functions for dealing with response formats for AWS API Gateway""" + import json from typing import Optional -def create_response( - status_code: int, - body: Optional[dict | str] = None, - headers: Optional[dict] = None -): +def create_response(status_code: int, body: Optional[dict | str] = None, headers: Optional[dict] = None): """Creates response body as per Lambda -> API Gateway proxy integration""" if body is not None: if isinstance(body, dict): diff --git a/backend/src/controller/constants.py b/backend/src/controller/constants.py index 7e6b2357e..868f0cc36 100644 --- a/backend/src/controller/constants.py +++ b/backend/src/controller/constants.py @@ -1,5 +1,4 @@ """FHIR Controller constants""" - SUPPLIER_SYSTEM_HEADER_NAME = "SupplierSystem" E_TAG_HEADER_NAME = "E-Tag" diff --git a/backend/src/controller/fhir_api_exception_handler.py b/backend/src/controller/fhir_api_exception_handler.py index fc793f683..3ec89d928 100644 --- a/backend/src/controller/fhir_api_exception_handler.py +++ b/backend/src/controller/fhir_api_exception_handler.py @@ -1,4 +1,5 @@ """Module for the global FHIR API exception handler""" + import functools import uuid from typing import Callable, Type @@ -6,14 +7,19 @@ from clients import logger from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.aws_apig_response_utils import create_response -from models.errors import UnauthorizedVaxError, UnauthorizedError, ResourceNotFoundError, create_operation_outcome, \ - Severity, Code - +from models.errors import ( + UnauthorizedVaxError, + UnauthorizedError, + ResourceNotFoundError, + create_operation_outcome, + Severity, + Code, +) _CUSTOM_EXCEPTION_TO_STATUS_MAP: dict[Type[Exception], int] = { UnauthorizedError: 403, UnauthorizedVaxError: 403, - ResourceNotFoundError: 404 + ResourceNotFoundError: 404, } @@ -39,4 +45,3 @@ def wrapper(*args, **kwargs): return create_response(500, server_error) return wrapper - diff --git a/backend/src/controller/fhir_batch_controller.py b/backend/src/controller/fhir_batch_controller.py index fbfb99a3f..22f67ca6c 100644 --- a/backend/src/controller/fhir_batch_controller.py +++ b/backend/src/controller/fhir_batch_controller.py @@ -1,7 +1,7 @@ """Function to send the request directly to lambda (or return appropriate diagnostics if this is not possible)""" -from service.fhir_batch_service import ImmunizationBatchService from repository.fhir_batch_repository import ImmunizationBatchRepository +from service.fhir_batch_service import ImmunizationBatchService def make_batch_controller(): @@ -33,5 +33,9 @@ def send_request_to_dynamo(self, message_body: dict, table: any, is_present: boo "DELETE": self.fhir_service.delete_immunization, } return function_map[operation_requested]( - immunization=fhir_json, supplier_system=supplier, vax_type=vax_type, table=table, is_present=is_present + immunization=fhir_json, + supplier_system=supplier, + vax_type=vax_type, + table=table, + is_present=is_present, ) diff --git a/backend/src/controller/fhir_controller.py b/backend/src/controller/fhir_controller.py index 2804a34ce..319d01f44 100644 --- a/backend/src/controller/fhir_controller.py +++ b/backend/src/controller/fhir_controller.py @@ -2,17 +2,20 @@ import json import os import re +import urllib.parse import uuid from decimal import Decimal from typing import Optional + from aws_lambda_typing.events import APIGatewayProxyEventV1 -from controller.aws_apig_event_utils import get_supplier_system_header, get_path_parameter +from controller.aws_apig_event_utils import ( + get_supplier_system_header, + get_path_parameter, +) from controller.aws_apig_response_utils import create_response from controller.constants import E_TAG_HEADER_NAME from controller.fhir_api_exception_handler import fhir_api_exception_handler -from repository.fhir_repository import ImmunizationRepository, create_table -from service.fhir_service import FhirService, UpdateOutcome, get_service_url from models.errors import ( Severity, Code, @@ -27,11 +30,14 @@ ) from models.utils.generic_utils import check_keys_in_sources from parameter_parser import process_params, process_search_params, create_query_string -import urllib.parse +from repository.fhir_repository import ImmunizationRepository, create_table +from service.fhir_service import FhirService, UpdateOutcome, get_service_url + +IMMUNIZATION_ENV = os.getenv("IMMUNIZATION_ENV") def make_controller( - immunization_env: str = os.getenv("IMMUNIZATION_ENV"), + immunization_env: str = IMMUNIZATION_ENV, ): endpoint_url = "http://localhost:4566" if immunization_env == "local" else None imms_repo = ImmunizationRepository(create_table(endpoint_url=endpoint_url)) @@ -80,7 +86,8 @@ def get_immunization_by_identifier(self, aws_event) -> dict: try: if resource := self.fhir_service.get_immunization_by_identifier( - identifiers, supplier_system, identifier, element): + identifiers, supplier_system, identifier, element + ): return create_response(200, resource) except UnauthorizedVaxError as unauthorized: return create_response(403, unauthorized.to_operation_outcome()) @@ -106,8 +113,8 @@ def create_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.forbidden, - diagnostics="Unauthorized request" - ) + diagnostics="Unauthorized request", + ), ) supplier_system = self._identify_supplier_system(aws_event) @@ -164,7 +171,10 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.invariant, - diagnostics=f"Validation errors: The provided immunization id:{imms_id} doesn't match with the content of the request body", + diagnostics=( + f"Validation errors: The provided immunization id:{imms_id} doesn't match with the content of " + "the request body" + ), ) return create_response(400, json.dumps(exp_error)) # Validate the imms id in the path params and body of request - end @@ -182,7 +192,9 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.not_found, - diagnostics=f"Validation errors: The requested immunization resource with id:{imms_id} was not found.", + diagnostics=( + f"Validation errors: The requested immunization resource with id:{imms_id} was not found." + ), ) return create_response(404, json.dumps(exp_error)) @@ -209,7 +221,7 @@ def update_immunization(self, aws_event): imms, existing_resource_version, existing_resource_vacc_type, - supplier_system + supplier_system, ) # Validate if the imms resource to be updated is a logically deleted resource-end else: @@ -219,7 +231,9 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.invariant, - diagnostics="Validation errors: Immunization resource version not specified in the request headers", + diagnostics=( + "Validation errors: Immunization resource version not specified in the request headers" + ), ) return create_response(400, json.dumps(exp_error)) # Validate if imms resource version is part of the request - end @@ -233,7 +247,10 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.invariant, - diagnostics=f"Validation errors: Immunization resource version:{resource_version} in the request headers is invalid.", + diagnostics=( + f"Validation errors: Immunization resource version:{resource_version} in the request " + "headers is invalid." + ), ) return create_response(400, json.dumps(exp_error)) # Validate the imms resource version provided in the request headers - end @@ -244,7 +261,10 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.invariant, - diagnostics=f"Validation errors: The requested immunization resource {imms_id} has changed since the last retrieve.", + diagnostics=( + f"Validation errors: The requested immunization resource {imms_id} has changed since the " + "last retrieve." + ), ) return create_response(400, json.dumps(exp_error)) if existing_resource_version < resource_version_header: @@ -252,19 +272,22 @@ def update_immunization(self, aws_event): resource_id=str(uuid.uuid4()), severity=Severity.error, code=Code.invariant, - diagnostics=f"Validation errors: The requested immunization resource {imms_id} version is inconsistent with the existing version.", + diagnostics=( + f"Validation errors: The requested immunization resource {imms_id} version is inconsistent " + "with the existing version." + ), ) return create_response(400, json.dumps(exp_error)) # Validate if resource version has changed since the last retrieve - end # Check if the record is reinstated record - start - if existing_record["Reinstated"] == True: + if existing_record["Reinstated"] is True: outcome, resource, updated_version = self.fhir_service.update_reinstated_immunization( imms_id, imms, existing_resource_version, existing_resource_vacc_type, - supplier_system + supplier_system, ) else: outcome, resource, updated_version = self.fhir_service.update_immunization( @@ -272,7 +295,7 @@ def update_immunization(self, aws_event): imms, existing_resource_version, existing_resource_vacc_type, - supplier_system + supplier_system, ) # Check if the record is reinstated record - end @@ -287,7 +310,9 @@ def update_immunization(self, aws_event): ) return create_response(400, json.dumps(exp_error)) if outcome == UpdateOutcome.UPDATE: - return create_response(200, None, {"E-Tag": updated_version}) #include e-tag here, is it not included in the response resource + return create_response( + 200, None, {"E-Tag": updated_version} + ) # include e-tag here, is it not included in the response resource except ValidationError as error: return create_response(400, error.to_operation_outcome()) except IdentifierDuplicationError as duplicate: @@ -317,7 +342,7 @@ def delete_immunization(self, aws_event): except ResourceNotFoundError as not_found: return create_response(404, not_found.to_operation_outcome()) except UnhandledResponseError as unhandled_error: - return create_response(500, unhandled_error.to_operation_outcome()) + return create_response(500, unhandled_error.to_operation_outcome()) except UnauthorizedVaxError as unauthorized: return create_response(403, unauthorized.to_operation_outcome()) @@ -366,9 +391,7 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict: for entry in result_json_dict["entry"] if entry["resource"].get("status") not in ("not-done", "entered-in-error") ] - total_count = sum( - 1 for entry in result_json_dict["entry"] if entry.get("search", {}).get("mode") == "match" - ) + total_count = sum(1 for entry in result_json_dict["entry"] if entry.get("search", {}).get("mode") == "match") result_json_dict["total"] = total_count if request_contained_unauthorised_vaccs: exp_error = create_operation_outcome( @@ -458,7 +481,13 @@ def fetch_identifier_system_and_element(self, event: dict): query_params = event.get("queryStringParameters", {}) body = event["body"] - not_required_keys = ["-date.from", "-date.to", "-immunization.target", "_include", "patient.identifier"] + not_required_keys = [ + "-date.from", + "-date.to", + "-immunization.target", + "_include", + "patient.identifier", + ] # Get Search Query Parameters if query_params and not body: diff --git a/backend/src/create_imms_handler.py b/backend/src/create_imms_handler.py index 630d6c32d..f61832123 100644 --- a/backend/src/create_imms_handler.py +++ b/backend/src/create_imms_handler.py @@ -3,16 +3,17 @@ import pprint import uuid +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.aws_apig_response_utils import create_response from controller.fhir_controller import FhirController, make_controller from local_lambda import load_string -from models.errors import Severity, Code, create_operation_outcome from log_structure import function_info -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE +from models.errors import Severity, Code, create_operation_outcome logging.basicConfig(level="INFO") logger = logging.getLogger() + @function_info def create_imms_handler(event, _context): return create_immunization(event, make_controller()) diff --git a/backend/src/delete_imms_handler.py b/backend/src/delete_imms_handler.py index 4838c7790..e621050d3 100644 --- a/backend/src/delete_imms_handler.py +++ b/backend/src/delete_imms_handler.py @@ -3,15 +3,16 @@ import pprint import uuid +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.aws_apig_response_utils import create_response from controller.fhir_controller import FhirController, make_controller -from models.errors import Severity, Code, create_operation_outcome from log_structure import function_info -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE +from models.errors import Severity, Code, create_operation_outcome logging.basicConfig(level="INFO") logger = logging.getLogger() + @function_info def delete_imms_handler(event, _context): return delete_immunization(event, make_controller()) @@ -40,7 +41,7 @@ def delete_immunization(event, controller: FhirController): "pathParameters": {"id": args.id}, "headers": { "Content-Type": "application/x-www-form-urlencoded", - "AuthenticationType": "ApplicationRestricted" + "AuthenticationType": "ApplicationRestricted", }, } pprint.pprint(delete_imms_handler(event, {})) diff --git a/backend/src/filter.py b/backend/src/filter.py index dcafa2889..8aa3f89d0 100644 --- a/backend/src/filter.py +++ b/backend/src/filter.py @@ -1,7 +1,11 @@ """Functions for filtering a FHIR Immunization Resource""" -from models.utils.generic_utils import is_actor_referencing_contained_resource, get_contained_practitioner, get_contained_patient from constants import Urls +from models.utils.generic_utils import ( + is_actor_referencing_contained_resource, + get_contained_practitioner, + get_contained_patient, +) def remove_reference_to_contained_practitioner(imms: dict) -> dict: @@ -14,7 +18,7 @@ def remove_reference_to_contained_practitioner(imms: dict) -> dict: # Remove reference to the contained practitioner from imms[performer] imms["performer"] = [ - x for x in imms["performer"] if not is_actor_referencing_contained_resource(x, contained_practitioner["id"]) + x for x in imms["performer"] if not is_actor_referencing_contained_resource(x, contained_practitioner["id"]) ] return imms diff --git a/backend/src/forwarding_batch_lambda.py b/backend/src/forwarding_batch_lambda.py index 14178b080..74cad1fbe 100644 --- a/backend/src/forwarding_batch_lambda.py +++ b/backend/src/forwarding_batch_lambda.py @@ -1,16 +1,19 @@ """Lambda Handler which streams batch file entries from Kinesis and forwards to the Imms FHIR API""" -import os -import simplejson as json import base64 -import time import logging +import os +import time from datetime import datetime +import simplejson as json + from batch.batch_filename_to_events_mapper import BatchFilenameToEventsMapper -from repository.fhir_batch_repository import create_table -from controller.fhir_batch_controller import ImmunizationBatchController, make_batch_controller from clients import sqs_client +from controller.fhir_batch_controller import ( + ImmunizationBatchController, + make_batch_controller, +) from models.errors import ( MessageNotSuccessfulError, RecordProcessorError, @@ -19,7 +22,7 @@ ResourceNotFoundError, ResourceFoundError, ) - +from repository.fhir_batch_repository import create_table logging.basicConfig(level="INFO") logger = logging.getLogger() @@ -50,7 +53,10 @@ def create_diagnostics_dictionary(error: Exception) -> dict: def forward_request_to_dynamo( - message_body: any, table: any, is_present: bool, batch_controller: ImmunizationBatchController + message_body: any, + table: any, + is_present: bool, + batch_controller: ImmunizationBatchController, ): """Forwards the request to the Imms API (where possible) and updates the ack file with the outcome""" row_id = message_body.get("row_id") @@ -106,18 +112,22 @@ def forward_lambda_handler(event, _): imms_id = forward_request_to_dynamo(incoming_message_body, table, identifier_already_present, controller) filename_to_events_mapper.add_event( - { **base_outgoing_message_body, - "operation_start_time": operation_start_time, - "operation_end_time": str(datetime.now()), - "imms_id": imms_id } + { + **base_outgoing_message_body, + "operation_start_time": operation_start_time, + "operation_end_time": str(datetime.now()), + "imms_id": imms_id, + } ) except Exception as error: # pylint: disable = broad-exception-caught filename_to_events_mapper.add_event( - { **base_outgoing_message_body, - "operation_start_time": operation_start_time, - "operation_end_time": str(datetime.now()), - "diagnostics": create_diagnostics_dictionary(error) } + { + **base_outgoing_message_body, + "operation_start_time": operation_start_time, + "operation_end_time": str(datetime.now()), + "diagnostics": create_diagnostics_dictionary(error), + } ) logger.error("Error processing message: %s", error) @@ -126,7 +136,11 @@ def forward_lambda_handler(event, _): sqs_message_body = json.dumps(events) logger.info(f"total message length:{len(sqs_message_body)}") - sqs_client.send_message(QueueUrl=QUEUE_URL, MessageBody=sqs_message_body, MessageGroupId=filename_key) + sqs_client.send_message( + QueueUrl=QUEUE_URL, + MessageBody=sqs_message_body, + MessageGroupId=filename_key, + ) if __name__ == "__main__": diff --git a/backend/src/get_imms_handler.py b/backend/src/get_imms_handler.py index 5c31f11f0..eb95f2f51 100644 --- a/backend/src/get_imms_handler.py +++ b/backend/src/get_imms_handler.py @@ -1,17 +1,14 @@ import argparse import logging import pprint -import uuid - from controller.fhir_controller import FhirController, make_controller -from models.errors import Severity, Code, create_operation_outcome from log_structure import function_info -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE logging.basicConfig(level="INFO") logger = logging.getLogger() + @function_info def get_imms_handler(event, _context): return get_immunization_by_id(event, make_controller()) diff --git a/backend/src/get_status_handler.py b/backend/src/get_status_handler.py index e3edb887d..764882eb6 100644 --- a/backend/src/get_status_handler.py +++ b/backend/src/get_status_handler.py @@ -1,6 +1,3 @@ def get_status_handler(_event, _context): - response = { - "statusCode": 200, # HTTP status code - "body": "OK" - } + response = {"statusCode": 200, "body": "OK"} # HTTP status code return response diff --git a/backend/src/log_firehose.py b/backend/src/log_firehose.py index 1f0189817..eebcb0cf5 100644 --- a/backend/src/log_firehose.py +++ b/backend/src/log_firehose.py @@ -1,9 +1,13 @@ -import boto3 -import logging import json +import logging import os + +import boto3 from botocore.config import Config +STREAM_NAME = os.getenv("SPLUNK_FIREHOSE_NAME") +BOTO_CLIENT = boto3.client("firehose", config=Config(region_name="eu-west-2")) + logging.basicConfig() logger = logging.getLogger() logger.setLevel("INFO") @@ -12,8 +16,8 @@ class FirehoseLogger: def __init__( self, - stream_name: str = os.getenv("SPLUNK_FIREHOSE_NAME"), - boto_client=boto3.client("firehose", config=Config(region_name="eu-west-2")), + stream_name: str = STREAM_NAME, + boto_client=BOTO_CLIENT, ): self.firehose_client = boto_client self.delivery_stream_name = stream_name @@ -24,8 +28,8 @@ def send_log(self, log_message): try: response = self.firehose_client.put_record( DeliveryStreamName=self.delivery_stream_name, - Record={"Data":encoded_log_data}, + Record={"Data": encoded_log_data}, ) logger.info(f"Log sent to Firehose: {response}") except Exception as e: - logger.exception(f"Error sending log to Firehose: {e}") \ No newline at end of file + logger.exception(f"Error sending log to Firehose: {e}") diff --git a/backend/src/log_structure.py b/backend/src/log_structure.py index e6b0d3c49..e819c8c9c 100644 --- a/backend/src/log_structure.py +++ b/backend/src/log_structure.py @@ -5,7 +5,6 @@ from functools import wraps from log_firehose import FirehoseLogger - from models.utils.validation_utils import get_vaccine_type logging.basicConfig() @@ -15,6 +14,7 @@ firehose_logger = FirehoseLogger() + def _log_data_from_body(event) -> dict: log_data = {} if event.get("body") is None: diff --git a/backend/src/models/constants.py b/backend/src/models/constants.py index c6799457e..005eba9a6 100644 --- a/backend/src/models/constants.py +++ b/backend/src/models/constants.py @@ -35,7 +35,15 @@ class Constants: "protocolApplied", }, "Practitioner": {"resourceType", "id", "name"}, - "Patient": {"resourceType", "id", "identifier", "name", "gender", "birthDate", "address"}, + "Patient": { + "resourceType", + "id", + "identifier", + "name", + "gender", + "birthDate", + "address", + }, } ALLOWED_CONTAINED_RESOURCES = {"Practitioner", "Patient"} diff --git a/backend/src/models/errors.py b/backend/src/models/errors.py index fe78581fb..8b2903243 100644 --- a/backend/src/models/errors.py +++ b/backend/src/models/errors.py @@ -25,7 +25,7 @@ class Code(str, Enum): class UnauthorizedError(RuntimeError): @staticmethod def to_operation_outcome() -> dict: - msg = f"Unauthorized request" + msg = "Unauthorized request" return create_operation_outcome( resource_id=str(uuid.uuid4()), severity=Severity.error, @@ -237,7 +237,7 @@ class UnauthorizedSystemError(RuntimeError): def __init__(self, message="Unauthorized system"): super().__init__(message) self.message = message - + def to_operation_outcome(self) -> dict: return create_operation_outcome( resource_id=str(uuid.uuid4()), diff --git a/backend/src/models/fhir_immunization.py b/backend/src/models/fhir_immunization.py index f2f116288..1f6e61867 100644 --- a/backend/src/models/fhir_immunization.py +++ b/backend/src/models/fhir_immunization.py @@ -1,8 +1,9 @@ """Immunization FHIR R4B validator""" from fhir.resources.R4B.immunization import Immunization -from models.fhir_immunization_pre_validators import PreValidators + from models.fhir_immunization_post_validators import PostValidators +from models.fhir_immunization_pre_validators import PreValidators from models.utils.validation_utils import get_vaccine_type @@ -57,8 +58,8 @@ def validate(self, immunization_json_data: dict) -> Immunization: # Post-FHIR validations if self.add_post_validators and not reduce_validation: - self.run_post_validators(immunization_json_data, vaccine_type) - + self.run_post_validators(immunization_json_data, vaccine_type) + def run_postalCode_validator(self, values: dict) -> None: """Run pre validation on the FHIR Immunization Resource JSON data""" if error := PreValidators.pre_validate_patient_address_postal_code(self, values): diff --git a/backend/src/models/fhir_immunization_post_validators.py b/backend/src/models/fhir_immunization_post_validators.py index cea4e7329..f4688de1d 100644 --- a/backend/src/models/fhir_immunization_post_validators.py +++ b/backend/src/models/fhir_immunization_post_validators.py @@ -1,11 +1,11 @@ "FHIR Immunization Post Validators" from models.errors import MandatoryError -from models.validation_sets import ValidationSets -from models.mandation_functions import MandationFunctions -from models.field_names import FieldNames from models.field_locations import FieldLocations +from models.field_names import FieldNames +from models.mandation_functions import MandationFunctions from models.utils.base_utils import obtain_field_value, obtain_field_location +from models.validation_sets import ValidationSets class PostValidators: @@ -126,7 +126,11 @@ def validate(self): mandation_functions = MandationFunctions(self.imms, self.vaccine_type) # Obtain the relevant validation set - validation_set = getattr(ValidationSets, self.vaccine_type.lower(), ValidationSets.vaccine_type_agnostic) + validation_set = getattr( + ValidationSets, + self.vaccine_type.lower(), + ValidationSets.vaccine_type_agnostic, + ) # Create an instance of FieldLocations and set dynamic fields field_locations = FieldLocations() diff --git a/backend/src/models/fhir_immunization_pre_validators.py b/backend/src/models/fhir_immunization_pre_validators.py index 0d81022aa..94e4435e4 100644 --- a/backend/src/models/fhir_immunization_pre_validators.py +++ b/backend/src/models/fhir_immunization_pre_validators.py @@ -1,6 +1,8 @@ "FHIR Immunization Pre Validators" -from typing import Union + +from constants import Urls from models.constants import Constants +from models.errors import MandatoryError from models.utils.generic_utils import ( get_generic_extension_value, generate_field_location_for_extension, @@ -12,8 +14,6 @@ patient_and_practitioner_value_and_index, ) from models.utils.pre_validator_utils import PreValidation -from models.errors import MandatoryError -from constants import Urls class PreValidators: @@ -239,9 +239,7 @@ def pre_validate_practitioner_reference(self, values: dict) -> dict: practitioner_references = [x for x in performer_internal_references if x == "#" + practitioner_id] if len(practitioner_references) == 0: - raise ValueError( - f"contained Practitioner resource id '{practitioner_id}' must be referenced from performer" - ) + raise ValueError(f"contained Practitioner resource id '{practitioner_id}' must be referenced from performer") elif len(practitioner_references) > 1: raise ValueError( f"contained Practitioner resource id '{practitioner_id}' must only be referenced once from performer" @@ -252,8 +250,6 @@ def pre_validate_patient_identifier_extension(self, values: dict) -> None: Pre-validate that if contained[?(@.resourceType=='Patient')].identifier[0] contains an extension field, it raises a validation error. """ - field_location = "contained[?(@.resourceType=='Patient')].identifier[0].extension" - try: patient = [x for x in values["contained"] if x.get("resourceType") == "Patient"][0] identifier = patient["identifier"][0] @@ -522,7 +518,7 @@ def pre_validate_recorded(self, values: dict) -> dict: """ try: recorded = values["recorded"] - PreValidation.for_date_time(recorded, "recorded",strict_timezone=False) + PreValidation.for_date_time(recorded, "recorded", strict_timezone=False) except KeyError: pass @@ -535,40 +531,50 @@ def pre_validate_primary_source(self, values: dict) -> dict: PreValidation.for_boolean(primary_source, "primarySource") except KeyError: pass - - def pre_validate_value_codeable_concept(self, values: dict) -> dict: """Pre-validate that valueCodeableConcept with coding exists within extension""" if "extension" not in values: raise MandatoryError("Validation errors: extension is a mandatory field") - + # Iterate over each extension and check for valueCodeableConcept and coding for extension in values["extension"]: if "valueCodeableConcept" not in extension: - raise MandatoryError("Validation errors: extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept is a mandatory field") - + raise MandatoryError( + "Validation errors: extension[?(@.url=='" + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + "')].valueCodeableConcept is a mandatory field" + ) + # Check that coding exists within valueCodeableConcept if "coding" not in extension["valueCodeableConcept"]: - raise MandatoryError("Validation errors: extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding is a mandatory field") - + raise MandatoryError( + "Validation errors: extension[?(@.url=='" + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + "')].valueCodeableConcept.coding is a mandatory field" + ) + def pre_validate_extension_length(self, values: dict) -> dict: - """Pre-validate that, if extension exists, then the length of the list should be 1""" - try: - field_value = values["extension"] - PreValidation.for_list(field_value, "extension", defined_length=1) - # Call the second validation method if the first validation passes - self.pre_validate_extension_url(values) - except KeyError: - pass + """Pre-validate that, if extension exists, then the length of the list should be 1""" + try: + field_value = values["extension"] + PreValidation.for_list(field_value, "extension", defined_length=1) + # Call the second validation method if the first validation passes + self.pre_validate_extension_url(values) + except KeyError: + pass def pre_validate_extension_url(self, values: dict) -> dict: - """Pre-validate that, if extension exists, then its url should be a valid one""" - try: - field_value = values["extension"][0]["url"] - PreValidation.for_string(field_value, "extension[0].url", predefined_values=Constants.EXTENSION_URL) - except KeyError: - pass + """Pre-validate that, if extension exists, then its url should be a valid one""" + try: + field_value = values["extension"][0]["url"] + PreValidation.for_string( + field_value, + "extension[0].url", + predefined_values=Constants.EXTENSION_URL, + ) + except KeyError: + pass def pre_validate_vaccination_procedure_code(self, values: dict) -> dict: """ @@ -626,7 +632,9 @@ def pre_validate_protocol_applied(self, values: dict) -> dict: PreValidation.for_list(field_value, "protocolApplied", defined_length=1) except KeyError: pass + DOSE_NUMBER_MAX_VALUE = 9 + def pre_validate_dose_number_positive_int(self, values: dict) -> dict: """ Pre-validate that, if protocolApplied[0].doseNumberPositiveInt (legacy CSV field : dose_sequence) @@ -824,10 +832,10 @@ def pre_validate_dose_quantity_system(self, values: dict) -> dict: """ try: field_value = values["doseQuantity"]["system"] - PreValidation.for_string(field_value, "doseQuantity.system") + PreValidation.for_string(field_value, "doseQuantity.system") except KeyError: pass - + def pre_validate_dose_quantity_code(self, values: dict) -> dict: """ Pre-validate that, if doseQuantity.code (legacy CSV field name: DOSE_UNIT_CODE) exists, @@ -847,11 +855,9 @@ def pre_validate_dose_quantity_system_and_code(self, values: dict) -> dict: dose_quantity = values.get("doseQuantity", {}) code = dose_quantity.get("code") system = dose_quantity.get("system") - - PreValidation.require_system_when_code_present( - code, system, "doseQuantity.code", "doseQuantity.system" - ) - + + PreValidation.require_system_when_code_present(code, system, "doseQuantity.code", "doseQuantity.system") + return values def pre_validate_dose_quantity_unit(self, values: dict) -> dict: @@ -946,4 +952,3 @@ def pre_validate_vaccine_code(self, values: dict) -> dict: PreValidation.for_snomed_code(field_value, field_location) except (KeyError, IndexError): pass - diff --git a/backend/src/models/field_locations.py b/backend/src/models/field_locations.py index b5e285d2a..9c7ef1e41 100644 --- a/backend/src/models/field_locations.py +++ b/backend/src/models/field_locations.py @@ -1,10 +1,11 @@ """ -File containing the field location strings for identifying the location of a field within the FHIR immunization +File containing the field location strings for identifying the location of a field within the FHIR immunization resource json data """ from dataclasses import dataclass, field +from constants import Urls from models.utils.generic_utils import ( generate_field_location_for_extension, patient_name_given_field_location, @@ -13,8 +14,6 @@ practitioner_name_family_field_location, ) -from constants import Urls - @dataclass class FieldLocations: @@ -31,7 +30,7 @@ class FieldLocations: patient_name_given: str = field(init=False) patient_name_family: str = field(init=False) patient_birth_date = "contained[?(@.resourceType=='Patient')].birthDate" - patient_gender = "contained[?(@.resourceType=='Patient')].gender" + patient_gender = "contained[?(@.resourceType=='Patient')].gender" patient_address_postal_code = "contained[?(@.resourceType=='Patient')].address[0].postalCode" occurrence_date_time = "occurrenceDateTime" organization_identifier_value = "performer[?(@.actor.type=='Organization')].actor.identifier.value" diff --git a/backend/src/models/obtain_field_value.py b/backend/src/models/obtain_field_value.py index 86a9c4376..3258c4bcf 100644 --- a/backend/src/models/obtain_field_value.py +++ b/backend/src/models/obtain_field_value.py @@ -1,18 +1,13 @@ """Functions for obtaining a field value from the FHIR immunization resource json data""" -from datetime import datetime +from constants import Urls from models.utils.generic_utils import ( get_contained_patient, get_contained_practitioner, is_organization, get_generic_extension_value, - generate_field_location_for_name, - get_occurrence_datetime, - obtain_current_name_period, - get_current_name_instance, patient_and_practitioner_value_and_index, ) -from constants import Urls class ObtainFieldValue: @@ -71,10 +66,10 @@ def patient_gender(imms: dict): def patient_address_postal_code(imms: dict): """Obtains patient_address_postal_code value""" patient = get_contained_patient(imms) - contained_patient_postalCode = [ - x for x in patient.get("address") if len(x.get("postalCode", "")) >= 1 - ][0]["postalCode"] - return contained_patient_postalCode + contained_patient_postal_code = [x for x in patient.get("address") if len(x.get("postalCode", "")) >= 1][0][ + "postalCode" + ] + return contained_patient_postal_code @staticmethod def organization_identifier_value(imms: dict): diff --git a/backend/src/models/utils/base_utils.py b/backend/src/models/utils/base_utils.py index 638721aa5..7314fa434 100644 --- a/backend/src/models/utils/base_utils.py +++ b/backend/src/models/utils/base_utils.py @@ -1,8 +1,12 @@ """Utils for backend src code""" -from models.obtain_field_value import ObtainFieldValue from models.field_locations import FieldLocations +from models.obtain_field_value import ObtainFieldValue + + +FIELD_LOCATIONS = FieldLocations() + def obtain_field_value(imms: dict, field_name: str) -> any: """Finds and returns the field value from the imms json data. Returns none if field not found.""" @@ -19,7 +23,7 @@ def obtain_field_value(imms: dict, field_name: str) -> any: return field_value -def obtain_field_location(field_name: str, field_locations: FieldLocations = FieldLocations()) -> str: +def obtain_field_location(field_name: str, field_locations: FieldLocations = FIELD_LOCATIONS) -> str: """ Obtains the field location of the given field from an instance of the FieldLocations class. NOTE: Some field locations need to be dynamically set. If this is required then diff --git a/backend/src/models/utils/generic_utils.py b/backend/src/models/utils/generic_utils.py index 04f4e36c1..1c91de47c 100644 --- a/backend/src/models/utils/generic_utils.py +++ b/backend/src/models/utils/generic_utils.py @@ -1,8 +1,11 @@ """Generic utilities""" +import base64 import datetime import json +import urllib.parse from typing import Literal, Union, Optional, Dict, Any + from fhir.resources.R4B.bundle import ( Bundle as FhirBundle, BundleEntry, @@ -10,11 +13,10 @@ BundleEntrySearch, ) from fhir.resources.R4B.immunization import Immunization -from models.constants import Constants -import urllib.parse -import base64 from stdnum.verhoeff import validate +from models.constants import Constants + def get_contained_resource(imms: dict, resource: Literal["Patient", "Practitioner", "QuestionnaireResponse"]): """Extract and return the requested contained resource from the FHIR Immunization Resource JSON data""" @@ -82,7 +84,7 @@ def is_valid_simple_snomed(simple_snomed: str) -> bool: return ( simple_snomed is not None and simple_snomed.isdigit() - and simple_snomed[0] != '0' + and simple_snomed[0] != "0" and min_snomed_length <= len(simple_snomed) <= max_snomed_length and validate(simple_snomed) and (simple_snomed[-3:-1] in ("00", "10")) @@ -126,29 +128,32 @@ def get_occurrence_datetime(immunization: dict) -> Optional[datetime.datetime]: def create_diagnostics(): - diagnostics = f"Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists." + diagnostics = "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists." exp_error = {"diagnostics": diagnostics} return exp_error + def create_diagnostics_error(value): if value == "Both": diagnostics = ( - f"Validation errors: identifier[0].system and identifier[0].value doesn't match with the stored content" + "Validation errors: identifier[0].system and identifier[0].value doesn't match with the stored content" ) else: diagnostics = f"Validation errors: identifier[0].{value} doesn't match with the stored content" exp_error = {"diagnostics": diagnostics} return exp_error + def make_empty_bundle(self_url: str) -> Dict[str, Any]: return { "resourceType": "Bundle", "type": "searchset", "link": [{"relation": "self", "url": self_url}], "entry": [], - "total": 0, + "total": 0, } + def form_json(response, _elements, identifier, baseurl): self_url = f"{baseurl}?identifier={identifier}" + (f"&_elements={_elements}" if _elements else "") @@ -157,33 +162,40 @@ def form_json(response, _elements, identifier, baseurl): meta = {"versionId": response["version"]} if "version" in response else {} - # Full Immunization payload to be returned if only the identifier parameter was provided and truncated when _elements is used + # Full Immunization payload to be returned if only the identifier parameter was provided and truncated + # when _elements is used if _elements: elements = {e.strip().lower() for e in _elements.split(",") if e.strip()} resource = {"resourceType": "Immunization"} - if "id" in elements: resource["id"] = response["id"] - if "meta" in elements and meta: resource["meta"] = meta + if "id" in elements: + resource["id"] = response["id"] + if "meta" in elements and meta: + resource["meta"] = meta else: resource = response["resource"] resource["meta"] = meta - entry = BundleEntry(fullUrl=f"{baseurl}/{response['id']}", - resource=Immunization.construct(**resource) if _elements else Immunization.parse_obj(resource), + entry = BundleEntry( + fullUrl=f"{baseurl}/{response['id']}", + resource=(Immunization.construct(**resource) if _elements else Immunization.parse_obj(resource)), search=BundleEntrySearch.construct(mode="match") if not _elements else None, ) fhir_bundle = FhirBundle( - resourceType="Bundle", type="searchset", - link = [BundleLink(relation="self", url=self_url)], - entry=[entry], - total=1) + resourceType="Bundle", + type="searchset", + link=[BundleLink(relation="self", url=self_url)], + entry=[entry], + total=1, + ) # Reassigned total to ensure it appears last in the response to match expected output data = json.loads(fhir_bundle.json(by_alias=True)) data["total"] = data.pop("total") return data + def check_keys_in_sources(event, not_required_keys): # Decode and parse the body, assuming it is JSON and base64-encoded def decode_and_parse_body(encoded_body): diff --git a/backend/src/models/utils/pre_validator_utils.py b/backend/src/models/utils/pre_validator_utils.py index 047fa9e58..2ac164d47 100644 --- a/backend/src/models/utils/pre_validator_utils.py +++ b/backend/src/models/utils/pre_validator_utils.py @@ -1,8 +1,6 @@ -import re -from datetime import datetime, timedelta +from datetime import datetime, date from decimal import Decimal from typing import Union -from datetime import datetime, date from .generic_utils import nhs_number_mod11_check, is_valid_simple_snomed @@ -94,9 +92,7 @@ def for_date(field_value: str, field_location: str, future_date_allowed: bool = try: parsed_date = datetime.strptime(field_value, "%Y-%m-%d").date() except ValueError as value_error: - raise ValueError( - f'{field_location} must be a valid date string in the format "YYYY-MM-DD"' - ) from value_error + raise ValueError(f'{field_location} must be a valid date string in the format "YYYY-MM-DD"') from value_error # Enforce future date rule using central checker after successful parse if not future_date_allowed and PreValidation.check_if_future_date(parsed_date): @@ -127,14 +123,20 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool = error_message += ( "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n" f"Note that partial dates are not allowed for {field_location} in this service.\n" - ) + ) - allowed_suffixes = {"+00:00", "+01:00", "+0000", "+0100",} + allowed_suffixes = { + "+00:00", + "+01:00", + "+0000", + "+0100", + } # List of accepted strict formats formats = [ "%Y-%m-%d", - "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z", + "%Y-%m-%dT%H:%M:%S%z", + "%Y-%m-%dT%H:%M:%S.%f%z", ] for fmt in formats: @@ -159,18 +161,15 @@ def for_snomed_code(field_value: str, field_location: str): Apply prevalidation to snomed code to ensure that its a valid one. """ - error_message = ( - f"{field_location} is not a valid snomed code" - ) - + error_message = f"{field_location} is not a valid snomed code" + try: - is_valid = is_valid_simple_snomed(field_value) + is_valid = is_valid_simple_snomed(field_value) except Exception: raise ValueError(error_message) if not is_valid: raise ValueError(error_message) - @staticmethod def for_boolean(field_value: str, field_location: str): """Apply pre-validation to a boolean field to ensure that it is a boolean""" @@ -183,7 +182,8 @@ def for_positive_integer(field_value: int, field_location: str, max_value: int = Apply pre-validation to an integer field to ensure that it is a positive integer, which does not exceed the maximum allowed value (if applicable) """ - if type(field_value) != int: # pylint: disable=unidiomatic-typecheck + # This check uses type() instead of isinstance() because bool is a subclass of int. + if type(field_value) is not int: # pylint: disable=unidiomatic-typecheck raise TypeError(f"{field_location} must be a positive integer") if field_value <= 0: @@ -200,18 +200,19 @@ def for_integer_or_decimal(field_value: Union[int, Decimal], field_location: str which does not exceed the maximum allowed number of decimal places (if applicable) """ if not ( + # This check uses type() instead of isinstance() because bool is a subclass of int. type(field_value) is int # pylint: disable=unidiomatic-typecheck or type(field_value) is Decimal # pylint: disable=unidiomatic-typecheck ): raise TypeError(f"{field_location} must be a number") - + @staticmethod def require_system_when_code_present( - code_value:str, - system_value:str, - code_location:str, - system_location:str, - ) -> None: + code_value: str, + system_value: str, + code_location: str, + system_location: str, + ) -> None: """ If code is present (non-empty), system must also be present (non-empty). """ @@ -256,4 +257,4 @@ def check_if_future_date(parsed_value: date | datetime): now = datetime.now().date() if parsed_value > now: return True - return False \ No newline at end of file + return False diff --git a/backend/src/models/utils/validation_utils.py b/backend/src/models/utils/validation_utils.py index 60eec9b6f..412332e92 100644 --- a/backend/src/models/utils/validation_utils.py +++ b/backend/src/models/utils/validation_utils.py @@ -2,15 +2,14 @@ import json -from typing import Union -from .generic_utils import create_diagnostics_error -from models.utils.base_utils import obtain_field_location -from models.obtain_field_value import ObtainFieldValue -from models.field_names import FieldNames -from models.errors import MandatoryError +from clients import redis_client from constants import Urls from models.constants import Constants -from clients import redis_client +from models.errors import MandatoryError +from models.field_names import FieldNames +from models.obtain_field_value import ObtainFieldValue +from models.utils.base_utils import obtain_field_location +from .generic_utils import create_diagnostics_error def get_target_disease_codes(immunization: dict): @@ -47,7 +46,9 @@ def get_target_disease_codes(immunization: dict): return target_disease_codes -def convert_disease_codes_to_vaccine_type(disease_codes_input: list) -> Union[str, None]: +def convert_disease_codes_to_vaccine_type( + disease_codes_input: list, +) -> str | None: """ Takes a list of disease codes and returns the corresponding vaccine type if found, otherwise raises a value error @@ -57,8 +58,9 @@ def convert_disease_codes_to_vaccine_type(disease_codes_input: list) -> Union[st if not vaccine_type: raise ValueError( - f"Validation errors: protocolApplied[0].targetDisease[*].coding[?(@.system=='http://snomed.info/sct')].code - " - f"{disease_codes_input} is not a valid combination of disease codes for this service" + "Validation errors: protocolApplied[0].targetDisease[*].coding[?(@.system=='" + "http://snomed.info/sct" + f"')].code - {disease_codes_input} is not a valid combination of disease codes for this service" ) return vaccine_type @@ -92,10 +94,7 @@ def check_identifier_system_value(response, imms: dict): identifier_system_response = resource["identifier"][0]["system"] identifier_value_response = resource["identifier"][0]["value"] - if ( - identifier_system_request != identifier_system_response - and identifier_value_request != identifier_value_response - ): + if identifier_system_request != identifier_system_response and identifier_value_request != identifier_value_response: value = "Both" diagnostics_error = create_diagnostics_error(value) return diagnostics_error diff --git a/backend/src/not_found_handler.py b/backend/src/not_found_handler.py index b8288345f..8acf38ab6 100644 --- a/backend/src/not_found_handler.py +++ b/backend/src/not_found_handler.py @@ -1,8 +1,10 @@ import json + from log_structure import function_info ALLOWED_METHODS = ["GET", "POST", "DELETE", "PUT"] + @function_info def not_found_handler(event, context): return not_found(event, context) @@ -22,9 +24,7 @@ def not_found(event, _context): "resourceType": "OperationOutcome", "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", "meta": { - "profile": [ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] + "profile": ["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"] }, "issue": [ { @@ -57,9 +57,7 @@ def not_found(event, _context): "resourceType": "OperationOutcome", "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", "meta": { - "profile": [ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] + "profile": ["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"] }, "issue": [ { diff --git a/backend/src/parameter_parser.py b/backend/src/parameter_parser.py index 1a88d56c0..21d996781 100644 --- a/backend/src/parameter_parser.py +++ b/backend/src/parameter_parser.py @@ -1,15 +1,17 @@ import base64 import datetime from dataclasses import dataclass - -from aws_lambda_typing.events import APIGatewayProxyEventV1 from typing import Optional from urllib.parse import parse_qs, urlencode, quote -from clients import redis_client, logger +from aws_lambda_typing.events import APIGatewayProxyEventV1 + +from clients import redis_client +from models.constants import Constants from models.errors import ParameterException from models.utils.generic_utils import nhs_number_mod11_check -from models.constants import Constants + +ERROR_MESSAGE_DUPLICATED_PARAMETERS = 'Parameters may not be duplicated. Use commas for "or".' ParamValue = list[str] ParamContainer = dict[str, ParamValue] @@ -36,6 +38,7 @@ class SearchParams: def __repr__(self): return str(self.__dict__) + def process_patient_identifier(identifier_params: ParamContainer) -> str: """Validate and parse patient identifier parameter. @@ -50,9 +53,11 @@ def process_patient_identifier(identifier_params: ParamContainer) -> str: patient_identifier_parts = patient_identifier.split("|") identifier_system = patient_identifier_parts[0] if len(patient_identifier_parts) != 2 or identifier_system != patient_identifier_system: - raise ParameterException("patient.identifier must be in the format of " - f"\"{patient_identifier_system}|{{NHS number}}\" " - f"e.g. \"{patient_identifier_system}|9000000009\"") + raise ParameterException( + "patient.identifier must be in the format of " + f'"{patient_identifier_system}|{{NHS number}}" ' + f'e.g. "{patient_identifier_system}|9000000009"' + ) nhs_number = patient_identifier_parts[1] if not nhs_number_mod11_check(nhs_number): @@ -66,16 +71,18 @@ def process_immunization_target(imms_params: ParamContainer) -> list[str]: :raises ParameterException: """ - vaccine_types = [vaccine_type for vaccine_type in set(imms_params.get(immunization_target_key, [])) if - vaccine_type is not None] + vaccine_types = [ + vaccine_type for vaccine_type in set(imms_params.get(immunization_target_key, [])) if vaccine_type is not None + ] if len(vaccine_types) < 1: raise ParameterException(f"Search parameter {immunization_target_key} must have one or more values.") valid_vaccine_types = redis_client.hkeys(Constants.VACCINE_TYPE_TO_DISEASES_HASH_KEY) if any(x not in valid_vaccine_types for x in vaccine_types): raise ParameterException( - f"immunization-target must be one or more of the following: {', '.join(valid_vaccine_types)}") - + f"immunization-target must be one or more of the following: {', '.join(valid_vaccine_types)}" + ) + return vaccine_types @@ -92,7 +99,9 @@ def process_mandatory_params(params: ParamContainer) -> tuple[str, list[str]]: return patient_identifier, vaccine_types -def process_optional_params(params: ParamContainer) -> tuple[datetime.date, datetime.date, Optional[str], list[str]]: +def process_optional_params( + params: ParamContainer, +) -> tuple[datetime.date, datetime.date, Optional[str], list[str]]: """Parse optional params (date.from, date.to, _include). Returns (date_from, date_to, include, errors). """ @@ -105,7 +114,9 @@ def process_optional_params(params: ParamContainer) -> tuple[datetime.date, date errors.append(f"Search parameter {date_from_key} may have one value at most.") try: - date_from = datetime.datetime.strptime(date_froms[0], "%Y-%m-%d").date() if len(date_froms) == 1 else date_from_default + date_from = ( + datetime.datetime.strptime(date_froms[0], "%Y-%m-%d").date() if len(date_froms) == 1 else date_from_default + ) except ValueError: errors.append(f"Search parameter {date_from_key} must be in format: YYYY-MM-DD") @@ -125,6 +136,7 @@ def process_optional_params(params: ParamContainer) -> tuple[datetime.date, date return date_from, date_to, include, errors + def process_search_params(params: ParamContainer) -> SearchParams: """Validate and parse search parameters. :raises ParameterException: @@ -145,17 +157,14 @@ def process_params(aws_event: APIGatewayProxyEventV1) -> ParamContainer: """Combines query string and content parameters. Duplicates not allowed. Splits on a comma.""" def split_and_flatten(input: list[str]): - return [x.strip() - for xs in input - for x in xs.split(",")] + return [x.strip() for xs in input for x in xs.split(",")] def parse_multi_value_query_parameters( - multi_value_query_params: dict[str, list[str]] + multi_value_query_params: dict[str, list[str]], ) -> ParamContainer: if any([len(v) > 1 for k, v in multi_value_query_params.items()]): - raise ParameterException("Parameters may not be duplicated. Use commas for \"or\".") - params = [(k, split_and_flatten(v)) - for k, v in multi_value_query_params.items()] + raise ParameterException(ERROR_MESSAGE_DUPLICATED_PARAMETERS) + params = [(k, split_and_flatten(v)) for k, v in multi_value_query_params.items()] return dict(params) @@ -168,7 +177,7 @@ def parse_body_params(aws_event: APIGatewayProxyEventV1) -> ParamContainer: parsed_body = parse_qs(decoded_body) if any([len(v) > 1 for k, v in parsed_body.items()]): - raise ParameterException("Parameters may not be duplicated. Use commas for \"or\".") + raise ParameterException(ERROR_MESSAGE_DUPLICATED_PARAMETERS) items = dict((k, split_and_flatten(v)) for k, v in parsed_body.items()) return items return {} @@ -177,25 +186,37 @@ def parse_body_params(aws_event: APIGatewayProxyEventV1) -> ParamContainer: body_params = parse_body_params(aws_event) if len(set(query_params.keys()) & set(body_params.keys())) > 0: - raise ParameterException("Parameters may not be duplicated. Use commas for \"or\".") + raise ParameterException(ERROR_MESSAGE_DUPLICATED_PARAMETERS) - parsed_params = {key: sorted(query_params.get(key, []) + body_params.get(key, [])) - for key in (query_params.keys() | body_params.keys())} + parsed_params = { + key: sorted(query_params.get(key, []) + body_params.get(key, [])) + for key in (query_params.keys() | body_params.keys()) + } return parsed_params def create_query_string(search_params: SearchParams) -> str: params = [ - (immunization_target_key, ",".join(map(quote, search_params.immunization_targets))), - (patient_identifier_key, - f"{patient_identifier_system}|{search_params.patient_identifier}"), - *([(date_from_key, search_params.date_from.isoformat())] - if search_params.date_from and search_params.date_from != date_from_default else []), - *([(date_to_key, search_params.date_to.isoformat())] - if search_params.date_to and search_params.date_to != date_to_default else []), - *([(include_key, search_params.include)] - if search_params.include else []), + ( + immunization_target_key, + ",".join(map(quote, search_params.immunization_targets)), + ), + ( + patient_identifier_key, + f"{patient_identifier_system}|{search_params.patient_identifier}", + ), + *( + [(date_from_key, search_params.date_from.isoformat())] + if search_params.date_from and search_params.date_from != date_from_default + else [] + ), + *( + [(date_to_key, search_params.date_to.isoformat())] + if search_params.date_to and search_params.date_to != date_to_default + else [] + ), + *([(include_key, search_params.include)] if search_params.include else []), ] search_params_qs = urlencode(sorted(params, key=lambda x: x[0]), safe=",") return search_params_qs diff --git a/backend/src/repository/fhir_batch_repository.py b/backend/src/repository/fhir_batch_repository.py index 3730adfe0..7e8ad4c53 100644 --- a/backend/src/repository/fhir_batch_repository.py +++ b/backend/src/repository/fhir_batch_repository.py @@ -1,13 +1,20 @@ import os -import uuid -import boto3 import time -import simplejson as json -from clients import logger +import uuid from dataclasses import dataclass + +import boto3 import botocore.exceptions +import simplejson as json from boto3.dynamodb.conditions import Key, Attr -from models.errors import UnhandledResponseError, IdentifierDuplicationError, ResourceNotFoundError, ResourceFoundError + +from clients import logger +from models.errors import ( + UnhandledResponseError, + IdentifierDuplicationError, + ResourceNotFoundError, + ResourceFoundError, +) def create_table(region_name="eu-west-2"): @@ -89,7 +96,12 @@ def __init__(self, imms: dict, vax_type: str, supplier: str, version: int): class ImmunizationBatchRepository: def create_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ) -> dict: new_id = str(uuid.uuid4()) immunization["id"] = new_id @@ -129,7 +141,12 @@ def create_immunization( ) def update_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ) -> dict: identifier = self._identifier_response(immunization) query_response = _query_identifier(table, "IdentifierGSI", "IdentifierPK", identifier, is_present) @@ -144,11 +161,20 @@ def update_immunization( update_exp = self._build_update_expression(is_reinstate=is_reinstate) return self._perform_dynamo_update( - update_exp, attr, deleted_at_required=deleted_at_required, update_reinstated=update_reinstated, table=table + update_exp, + attr, + deleted_at_required=deleted_at_required, + update_reinstated=update_reinstated, + table=table, ) def delete_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ) -> dict: identifier = self._identifier_response(immunization) query_response = _query_identifier(table, "IdentifierGSI", "IdentifierPK", identifier, is_present) @@ -249,7 +275,7 @@ def _perform_dynamo_update( if deleted_at_required else Attr("PK").eq(attr.pk) & Attr("DeletedAt").not_exists() ) - if deleted_at_required and update_reinstated == False: + if deleted_at_required and update_reinstated is False: expression_attribute_values = { ":timestamp": attr.timestamp, ":patient_pk": attr.patient_pk, diff --git a/backend/src/repository/fhir_repository.py b/backend/src/repository/fhir_repository.py index 80ebe765e..efc133c8f 100644 --- a/backend/src/repository/fhir_repository.py +++ b/backend/src/repository/fhir_repository.py @@ -1,5 +1,3 @@ -from responses import logger -import simplejson as json import os import time import uuid @@ -8,16 +6,21 @@ import boto3 import botocore.exceptions +import simplejson as json from boto3.dynamodb.conditions import Attr, Key from botocore.config import Config +from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table +from responses import logger + from models.errors import ( ResourceNotFoundError, UnhandledResponseError, IdentifierDuplicationError, ) -from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table - -from models.utils.validation_utils import get_vaccine_type, check_identifier_system_value +from models.utils.validation_utils import ( + get_vaccine_type, + check_identifier_system_value, +) def create_table(table_name=None, endpoint_url=None, region_name="eu-west-2"): @@ -86,7 +89,8 @@ def __init__(self, table: Table): def get_immunization_by_identifier(self, identifier_pk: str) -> tuple[Optional[dict], Optional[str]]: response = self.table.query( - IndexName="IdentifierGSI", KeyConditionExpression=Key("IdentifierPK").eq(identifier_pk) + IndexName="IdentifierGSI", + KeyConditionExpression=Key("IdentifierPK").eq(identifier_pk), ) if "Items" in response and len(response["Items"]) > 0: @@ -97,7 +101,7 @@ def get_immunization_by_identifier(self, identifier_pk: str) -> tuple[Optional[d return { "resource": resource, "id": resource.get("id"), - "version": version + "version": version, }, vaccine_type else: return None, None @@ -284,7 +288,7 @@ def _perform_dynamo_update( if deleted_at_required else Attr("PK").eq(attr.pk) & Attr("DeletedAt").not_exists() ) - if deleted_at_required and update_reinstated == False: + if deleted_at_required and update_reinstated is False: ExpressionAttributeValues = { ":timestamp": attr.timestamp, ":patient_pk": attr.patient_pk, @@ -343,8 +347,8 @@ def delete_immunization(self, imms_id: str, supplier_system: str) -> dict: }, ReturnValues="ALL_NEW", ConditionExpression=( - Attr("PK").eq(_make_immunization_pk(imms_id)) & - (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")) + Attr("PK").eq(_make_immunization_pk(imms_id)) + & (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")) ), ) @@ -370,12 +374,13 @@ def find_immunizations(self, patient_identifier: str, vaccine_types: set): items = [x for x in raw_items if x["PatientSK"].split("#")[0] in vaccine_types] # Return a list of the FHIR immunization resource JSON items - final_resources = [{ - **json.loads(item["Resource"]), - "meta": {"versionId": int(item.get("Version", 1))} + final_resources = [ + { + **json.loads(item["Resource"]), + "meta": {"versionId": int(item.get("Version", 1))}, } for item in items - ] + ] return final_resources else: diff --git a/backend/src/search_imms_handler.py b/backend/src/search_imms_handler.py index 513727ec7..7254566a1 100644 --- a/backend/src/search_imms_handler.py +++ b/backend/src/search_imms_handler.py @@ -1,22 +1,23 @@ import argparse +import base64 import json import logging import pprint +import urllib.parse import uuid from aws_lambda_typing import context as context_, events +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE, MAX_RESPONSE_SIZE_BYTES from controller.aws_apig_response_utils import create_response from controller.fhir_controller import FhirController, make_controller -from models.errors import Severity, Code, create_operation_outcome -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE, MAX_RESPONSE_SIZE_BYTES from log_structure import function_info -import base64 -import urllib.parse +from models.errors import Severity, Code, create_operation_outcome logging.basicConfig(level="INFO") logger = logging.getLogger() + @function_info def search_imms_handler(event: events.APIGatewayProxyEventV1, _context: context_): return search_imms(event, make_controller()) @@ -30,11 +31,9 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController query_string_has_immunization_identifier = False query_string_has_element = False body_has_immunization_element = False - if not (query_params == None and body == None): + if not (query_params is None and body is None): if query_params: - query_string_has_immunization_identifier = "identifier" in event.get( - "queryStringParameters", {} - ) + query_string_has_immunization_identifier = "identifier" in event.get("queryStringParameters", {}) query_string_has_element = "_elements" in event.get("queryStringParameters", {}) # Decode body from base64 if body: @@ -103,7 +102,13 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController required=False, dest="identifier", ) - parser.add_argument("--elements", help="Identifier of System", type=str, required=False, dest="_elements") + parser.add_argument( + "--elements", + help="Identifier of System", + type=str, + required=False, + dest="_elements", + ) args = parser.parse_args() event: events.APIGatewayProxyEventV1 = { @@ -113,13 +118,13 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController "-date.from": [args.date_from] if args.date_from else [], "-date.to": [args.date_to] if args.date_to else [], "_include": ["Immunization:patient"], - "identifier": [args.immunization_identifier] if args.immunization_identifier else [], + "identifier": ([args.immunization_identifier] if args.immunization_identifier else []), "_elements": [args._element] if args._element else [], }, "httpMethod": "POST", "headers": { "Content-Type": "application/x-www-form-urlencoded", - "AuthenticationType": "ApplicationRestricted" + "AuthenticationType": "ApplicationRestricted", }, "body": None, "resource": None, diff --git a/backend/src/service/fhir_batch_service.py b/backend/src/service/fhir_batch_service.py index c618c4c95..85e39a1e8 100644 --- a/backend/src/service/fhir_batch_service.py +++ b/backend/src/service/fhir_batch_service.py @@ -1,21 +1,30 @@ from pydantic import ValidationError -from repository.fhir_batch_repository import ImmunizationBatchRepository + from models.errors import CustomValidationError -from models.fhir_immunization import ImmunizationValidator from models.errors import MandatoryError +from models.fhir_immunization import ImmunizationValidator + +from repository.fhir_batch_repository import ImmunizationBatchRepository + +IMMUNIZATION_VALIDATOR = ImmunizationValidator() class ImmunizationBatchService: def __init__( self, immunization_repo: ImmunizationBatchRepository, - validator: ImmunizationValidator = ImmunizationValidator(), + validator: ImmunizationValidator = IMMUNIZATION_VALIDATOR, ): self.immunization_repo = immunization_repo self.validator = validator def create_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ): """ Creates an Immunization if it does not exits and return the ID back if successful. @@ -27,12 +36,15 @@ def create_immunization( except (ValidationError, ValueError, MandatoryError) as error: raise CustomValidationError(message=str(error)) from error - return self.immunization_repo.create_immunization( - immunization, supplier_system, vax_type, table, is_present - ) + return self.immunization_repo.create_immunization(immunization, supplier_system, vax_type, table, is_present) def update_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ): """ Updates an Immunization if it exists and return the ID back if successful. @@ -44,18 +56,19 @@ def update_immunization( except (ValidationError, ValueError, MandatoryError) as error: raise CustomValidationError(message=str(error)) from error - return self.immunization_repo.update_immunization( - immunization, supplier_system, vax_type, table, is_present - ) + return self.immunization_repo.update_immunization(immunization, supplier_system, vax_type, table, is_present) def delete_immunization( - self, immunization: any, supplier_system: str, vax_type: str, table: any, is_present: bool + self, + immunization: any, + supplier_system: str, + vax_type: str, + table: any, + is_present: bool, ): """ Delete an Immunization if it exists and return the ID back if successful. Exception will be raised if resource didn't exist.Multiple calls to this method won't change the record in the database. """ - return self.immunization_repo.delete_immunization( - immunization, supplier_system, vax_type, table, is_present - ) + return self.immunization_repo.delete_immunization(immunization, supplier_system, vax_type, table, is_present) diff --git a/backend/src/service/fhir_service.py b/backend/src/service/fhir_service.py index 0e4c50921..45616521c 100644 --- a/backend/src/service/fhir_service.py +++ b/backend/src/service/fhir_service.py @@ -1,9 +1,11 @@ -import logging -from uuid import uuid4 import datetime +import logging import os + + from enum import Enum from typing import Optional, Union +from uuid import uuid4 from fhir.resources.R4B.bundle import ( Bundle as FhirBundle, @@ -17,20 +19,41 @@ import parameter_parser from authorisation.api_operation_code import ApiOperationCode from authorisation.authoriser import Authoriser -from repository.fhir_repository import ImmunizationRepository -from models.errors import InvalidPatientId, CustomValidationError, UnauthorizedVaxError, ResourceNotFoundError -from models.fhir_immunization import ImmunizationValidator -from models.utils.generic_utils import nhs_number_mod11_check, get_occurrence_datetime, create_diagnostics, form_json, get_contained_patient + +from filter import Filter +from models.errors import ( + InvalidPatientId, + CustomValidationError, + UnauthorizedVaxError, + ResourceNotFoundError, +) from models.errors import MandatoryError +from models.fhir_immunization import ImmunizationValidator + +from models.utils.generic_utils import ( + nhs_number_mod11_check, + get_occurrence_datetime, + form_json, + get_contained_patient, +) from models.utils.validation_utils import get_vaccine_type +from repository.fhir_repository import ImmunizationRepository from timer import timed -from filter import Filter logging.basicConfig(level="INFO") logger = logging.getLogger() -def get_service_url(service_env: str = os.getenv("IMMUNIZATION_ENV"), service_base_path: str = os.getenv("IMMUNIZATION_BASE_PATH") - ) -> str: +IMMUNIZATION_BASE_PATH = os.getenv("IMMUNIZATION_BASE_PATH") +IMMUNIZATION_ENV = os.getenv("IMMUNIZATION_ENV") + +AUTHORISER = Authoriser() +IMMUNIZATION_VALIDATOR = ImmunizationValidator() + + +def get_service_url( + service_env: str = IMMUNIZATION_ENV, + service_base_path: str = IMMUNIZATION_BASE_PATH, +) -> str: if not service_base_path: service_base_path = "immunisation-fhir-api/FHIR/R4" @@ -55,8 +78,8 @@ class FhirService: def __init__( self, imms_repo: ImmunizationRepository, - authoriser: Authoriser = Authoriser(), - validator: ImmunizationValidator = ImmunizationValidator(), + authoriser: Authoriser = AUTHORISER, + validator: ImmunizationValidator = IMMUNIZATION_VALIDATOR, ): self.authoriser = authoriser self.immunization_repo = imms_repo @@ -79,8 +102,8 @@ def get_immunization_by_identifier( raise UnauthorizedVaxError() patient_full_url = f"urn:uuid:{str(uuid4())}" - filtered_resource = Filter.search(imms_resp['resource'], patient_full_url) - imms_resp['resource'] = filtered_resource + filtered_resource = Filter.search(imms_resp["resource"], patient_full_url) + imms_resp["resource"] = filtered_resource return form_json(imms_resp, element, identifier, base_url) def get_immunization_and_version_by_id(self, imms_id: str, supplier_system: str) -> tuple[Immunization, str]: @@ -152,16 +175,15 @@ def update_immunization( # If the user is updating the resource vaccination_type, they must have permissions for both the existing and # new type. In most cases it will be the same, but it is possible for users to update the vacc type - if not self.authoriser.authorise(supplier_system, ApiOperationCode.UPDATE, - {vaccination_type, existing_resource_vacc_type}): + if not self.authoriser.authorise( + supplier_system, + ApiOperationCode.UPDATE, + {vaccination_type, existing_resource_vacc_type}, + ): raise UnauthorizedVaxError() imms, updated_version = self.immunization_repo.update_immunization( - imms_id, - immunization, - patient, - existing_resource_version, - supplier_system + imms_id, immunization, patient, existing_resource_version, supplier_system ) return UpdateOutcome.UPDATE, Immunization.parse_obj(imms), updated_version @@ -181,16 +203,15 @@ def reinstate_immunization( vaccination_type = get_vaccine_type(immunization) - if not self.authoriser.authorise(supplier_system, ApiOperationCode.UPDATE, - {vaccination_type, existing_resource_vacc_type}): + if not self.authoriser.authorise( + supplier_system, + ApiOperationCode.UPDATE, + {vaccination_type, existing_resource_vacc_type}, + ): raise UnauthorizedVaxError() imms, updated_version = self.immunization_repo.reinstate_immunization( - imms_id, - immunization, - patient, - existing_resource_version, - supplier_system + imms_id, immunization, patient, existing_resource_version, supplier_system ) return UpdateOutcome.UPDATE, Immunization.parse_obj(imms), updated_version @@ -210,8 +231,11 @@ def update_reinstated_immunization( vaccination_type = get_vaccine_type(immunization) - if not self.authoriser.authorise(supplier_system, ApiOperationCode.UPDATE, - {vaccination_type, existing_resource_vacc_type}): + if not self.authoriser.authorise( + supplier_system, + ApiOperationCode.UPDATE, + {vaccination_type, existing_resource_vacc_type}, + ): raise UnauthorizedVaxError() imms, updated_version = self.immunization_repo.update_reinstated_immunization( @@ -240,9 +264,7 @@ def delete_immunization(self, imms_id: str, supplier_system: str) -> Immunizatio if not self.authoriser.authorise(supplier_system, ApiOperationCode.DELETE, {vaccination_type}): raise UnauthorizedVaxError() - imms = self.immunization_repo.delete_immunization( - imms_id, supplier_system - ) + imms = self.immunization_repo.delete_immunization(imms_id, supplier_system) return Immunization.parse_obj(imms) @staticmethod @@ -309,7 +331,7 @@ def create_url_for_bundle_link(params, vaccine_types): # Update the immunization.target parameter new_immunization_target_param = f"immunization.target={','.join(vaccine_types)}" parameters = "&".join( - [new_immunization_target_param if x.startswith("-immunization.target=") else x for x in params.split("&")] + [(new_immunization_target_param if x.startswith("-immunization.target=") else x) for x in params.split("&")] ) return f"{base_url}?{parameters}" @@ -377,10 +399,12 @@ def search_immunizations( # Create the bundle fhir_bundle = FhirBundle(resourceType="Bundle", type="searchset", entry=entries) - fhir_bundle.link = [BundleLink( - relation="self", - url=self.create_url_for_bundle_link(params, permitted_vacc_types) - )] + fhir_bundle.link = [ + BundleLink( + relation="self", + url=self.create_url_for_bundle_link(params, permitted_vacc_types), + ) + ] supplier_requested_unauthorised_vaccs = len(vaccine_types) != len(permitted_vacc_types) return fhir_bundle, supplier_requested_unauthorised_vaccs diff --git a/backend/src/timer.py b/backend/src/timer.py index 0d32f6369..c78fda30d 100644 --- a/backend/src/timer.py +++ b/backend/src/timer.py @@ -15,7 +15,7 @@ def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() - log = {"time_taken":"{} ran in {}s".format(func.__name__, round(end - start, 5))} + log = {"time_taken": "{} ran in {}s".format(func.__name__, round(end - start, 5))} logger.info(log) return result diff --git a/backend/src/update_imms_handler.py b/backend/src/update_imms_handler.py index e8af7f06b..cee122a55 100644 --- a/backend/src/update_imms_handler.py +++ b/backend/src/update_imms_handler.py @@ -3,16 +3,17 @@ import pprint import uuid +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.aws_apig_response_utils import create_response from controller.fhir_controller import FhirController, make_controller from local_lambda import load_string -from models.errors import Severity, Code, create_operation_outcome from log_structure import function_info -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE +from models.errors import Severity, Code, create_operation_outcome logging.basicConfig(level="INFO") logger = logging.getLogger() + @function_info def update_imms_handler(event, _context): return update_imms(event, make_controller()) @@ -43,7 +44,7 @@ def update_imms(event, controller: FhirController): "body": load_string(args.path), "headers": { "Content-Type": "application/x-www-form-urlencoded", - "AuthenticationType": "ApplicationRestricted" + "AuthenticationType": "ApplicationRestricted", }, } diff --git a/backend/src/utils/dict_utils.py b/backend/src/utils/dict_utils.py index 2874dc07f..1bcff54ac 100644 --- a/backend/src/utils/dict_utils.py +++ b/backend/src/utils/dict_utils.py @@ -1,4 +1,5 @@ """Generic helper module for Python dictionary utility functions""" + from typing import Optional, Any diff --git a/backend/tests/authorisation/test_authoriser.py b/backend/tests/authorisation/test_authoriser.py index 6d706a5e2..1845736a5 100644 --- a/backend/tests/authorisation/test_authoriser.py +++ b/backend/tests/authorisation/test_authoriser.py @@ -34,7 +34,7 @@ def test_authorise_returns_true_if_supplier_has_permissions(self): def test_authorise_returns_false_if_supplier_does_not_have_any_permissions(self): """Authoriser().authorise should return false if the supplier does not have any permissions in the cache""" - self.mock_cache_client.hget.return_value = '' + self.mock_cache_client.hget.return_value = "" result = self.test_authoriser.authorise(self.MOCK_SUPPLIER_NAME, ApiOperationCode.CREATE, {"COVID19"}) @@ -44,7 +44,9 @@ def test_authorise_returns_false_if_supplier_does_not_have_any_permissions(self) "operation: c, supplier_permissions: {}, vaccine_types: {'COVID19'}" ) - def test_authorise_returns_false_if_supplier_does_not_have_permission_for_operation(self): + def test_authorise_returns_false_if_supplier_does_not_have_permission_for_operation( + self, + ): """Authoriser().authorise should return false if the supplier does not have permission for the operation""" self.mock_cache_client.hget.return_value = '["COVID19.RS"]' @@ -74,31 +76,38 @@ def test_authorise_returns_false_multiple_vaccs_scenario(self): the list provided""" self.mock_cache_client.hget.return_value = '["COVID19.RS", "FLU.CRUDS"]' - result = self.test_authoriser.authorise(self.MOCK_SUPPLIER_NAME, ApiOperationCode.READ, { - "FLU", "COVID19", "RSV"}) + result = self.test_authoriser.authorise( + self.MOCK_SUPPLIER_NAME, ApiOperationCode.READ, {"FLU", "COVID19", "RSV"} + ) self.assertFalse(result) self.mock_cache_client.hget.assert_called_once_with("supplier_permissions", self.MOCK_SUPPLIER_NAME) - def test_filter_permitted_vacc_types_returns_all_if_supplier_has_perms_for_all(self): + def test_filter_permitted_vacc_types_returns_all_if_supplier_has_perms_for_all( + self, + ): """The same set of vaccination types will be returned if the supplier has the required permissions""" self.mock_cache_client.hget.return_value = '["COVID19.RS", "FLU.CRUDS", "RSV.CRUDS"]' requested_vacc_types = {"FLU", "COVID19", "RSV"} - result = self.test_authoriser.filter_permitted_vacc_types(self.MOCK_SUPPLIER_NAME, ApiOperationCode.SEARCH, - requested_vacc_types) + result = self.test_authoriser.filter_permitted_vacc_types( + self.MOCK_SUPPLIER_NAME, ApiOperationCode.SEARCH, requested_vacc_types + ) self.assertSetEqual(result, requested_vacc_types) self.mock_cache_client.hget.assert_called_once_with("supplier_permissions", self.MOCK_SUPPLIER_NAME) self.assertNotEqual(id(requested_vacc_types), id(result)) - def test_filter_permitted_vacc_types_removes_any_vacc_types_that_the_supplier_cannot_interact_with(self): + def test_filter_permitted_vacc_types_removes_any_vacc_types_that_the_supplier_cannot_interact_with( + self, + ): """Filter permitted vacc types method will filter out any vaccination types that the user cannot interact with""" self.mock_cache_client.hget.return_value = '["COVID19.RS", "FLU.CRUDS", "RSV.R"]' - result = self.test_authoriser.filter_permitted_vacc_types(self.MOCK_SUPPLIER_NAME, ApiOperationCode.SEARCH, { - "FLU", "COVID19", "RSV"}) + result = self.test_authoriser.filter_permitted_vacc_types( + self.MOCK_SUPPLIER_NAME, ApiOperationCode.SEARCH, {"FLU", "COVID19", "RSV"} + ) self.assertSetEqual(result, {"FLU", "COVID19"}) self.mock_cache_client.hget.assert_called_once_with("supplier_permissions", self.MOCK_SUPPLIER_NAME) diff --git a/backend/tests/batch/test_batch_filename_to_events_mapper.py b/backend/tests/batch/test_batch_filename_to_events_mapper.py index 1faea37f3..4e918484d 100644 --- a/backend/tests/batch/test_batch_filename_to_events_mapper.py +++ b/backend/tests/batch/test_batch_filename_to_events_mapper.py @@ -10,7 +10,7 @@ "supplier": "supplier_one", "vax_type": "RSV", "local_id": "local-1", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } MOCK_SUPPLIER_ONE_RSV_EVENT_TWO = { @@ -20,7 +20,7 @@ "supplier": "supplier_one", "vax_type": "RSV", "local_id": "local-2", - "operation_requested": "UPDATE" + "operation_requested": "UPDATE", } MOCK_SUPPLIER_TWO_COVID_EVENT_ONE = { @@ -30,7 +30,7 @@ "supplier": "supplier_two", "vax_type": "COVID-19", "local_id": "local-1", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } @@ -56,10 +56,10 @@ def test_add_event_appends_to_existing_key(self): result = self.batch_filename_to_events_mapper.get_map() self.assertIn(self.expected_key_supplier_one, result) - self.assertEqual(result[self.expected_key_supplier_one], [ - MOCK_SUPPLIER_ONE_RSV_EVENT_ONE, - MOCK_SUPPLIER_ONE_RSV_EVENT_TWO - ]) + self.assertEqual( + result[self.expected_key_supplier_one], + [MOCK_SUPPLIER_ONE_RSV_EVENT_ONE, MOCK_SUPPLIER_ONE_RSV_EVENT_TWO], + ) def test_mapper_handles_events_from_multiple_files(self): self.batch_filename_to_events_mapper.add_event(MOCK_SUPPLIER_ONE_RSV_EVENT_ONE) @@ -70,10 +70,10 @@ def test_mapper_handles_events_from_multiple_files(self): self.assertEqual(len(result.keys()), 2) self.assertIn(self.expected_key_supplier_one, result) - self.assertEqual(result[self.expected_key_supplier_one], [ - MOCK_SUPPLIER_ONE_RSV_EVENT_ONE, - MOCK_SUPPLIER_ONE_RSV_EVENT_TWO - ]) + self.assertEqual( + result[self.expected_key_supplier_one], + [MOCK_SUPPLIER_ONE_RSV_EVENT_ONE, MOCK_SUPPLIER_ONE_RSV_EVENT_TWO], + ) self.assertIn(self.expected_key_supplier_two, result) self.assertEqual(result[self.expected_key_supplier_two], [MOCK_SUPPLIER_TWO_COVID_EVENT_ONE]) diff --git a/backend/tests/controller/test_fhir_api_exception_handler.py b/backend/tests/controller/test_fhir_api_exception_handler.py index eb4438437..bcc6d4792 100644 --- a/backend/tests/controller/test_fhir_api_exception_handler.py +++ b/backend/tests/controller/test_fhir_api_exception_handler.py @@ -16,6 +16,7 @@ def tearDown(self): def test_exception_handler_does_nothing_when_no_exception_occurs(self): """Test that when the wrapped function returns successfully then the wrapper does nothing""" + @fhir_api_exception_handler def dummy_func(): return "Hello World" @@ -27,17 +28,26 @@ def test_exception_handler_handles_custom_exception_and_returns_fhir_response(se """Test that custom exceptions are handled by the wrapper and a valid response is returned to the client""" test_cases = [ (UnauthorizedError(), 403, "forbidden", "Unauthorized request"), - (UnauthorizedVaxError(), 403, "forbidden", "Unauthorized request for vaccine type"), - (ResourceNotFoundError(resource_type="Immunization", resource_id="123"), 404, "not-found", - "Immunization resource does not exist. ID: 123") + ( + UnauthorizedVaxError(), + 403, + "forbidden", + "Unauthorized request for vaccine type", + ), + ( + ResourceNotFoundError(resource_type="Immunization", resource_id="123"), + 404, + "not-found", + "Immunization resource does not exist. ID: 123", + ), ] for error, expected_status, expected_code, expected_message in test_cases: with self.subTest(msg=f"Test {error.__class__.__name__}"): @fhir_api_exception_handler - def dummy_func(): - raise error + def dummy_func(e=error): + raise e response = dummy_func() @@ -49,10 +59,10 @@ def dummy_func(): self.assertEqual(operation_outcome["issue"][0]["code"], expected_code) self.assertEqual(operation_outcome["issue"][0]["diagnostics"], expected_message) - def test_exception_handler_logs_exception_when_unexpected_error_occurs(self): """Test that when an unexpected exception occurs the exception is logged and an appropriate response is returned""" + @fhir_api_exception_handler def dummy_func(): raise Exception("Something went very wrong") @@ -67,5 +77,5 @@ def dummy_func(): self.assertEqual(operation_outcome["issue"][0]["code"], "exception") self.assertEqual( operation_outcome["issue"][0]["diagnostics"], - "Unable to process request. Issue may be transient." + "Unable to process request. Issue may be transient.", ) diff --git a/backend/tests/controller/test_fhir_batch_controller.py b/backend/tests/controller/test_fhir_batch_controller.py index 084291f8e..b06bc9164 100644 --- a/backend/tests/controller/test_fhir_batch_controller.py +++ b/backend/tests/controller/test_fhir_batch_controller.py @@ -1,16 +1,18 @@ import unittest import uuid from unittest.mock import Mock, create_autospec -from testing_utils.immunization_utils import create_covid_19_immunization -from service.fhir_batch_service import ImmunizationBatchService -from repository.fhir_batch_repository import ImmunizationBatchRepository + +from controller.fhir_batch_controller import ImmunizationBatchController from models.errors import ( ResourceNotFoundError, UnhandledResponseError, CustomValidationError, - IdentifierDuplicationError + IdentifierDuplicationError, ) -from controller.fhir_batch_controller import ImmunizationBatchController +from repository.fhir_batch_repository import ImmunizationBatchRepository +from service.fhir_batch_service import ImmunizationBatchService +from testing_utils.immunization_utils import create_covid_19_immunization + class TestCreateImmunizationBatchController(unittest.TestCase): @@ -18,10 +20,7 @@ def setUp(self): self.mock_repo = create_autospec(ImmunizationBatchRepository) self.mock_service = create_autospec(ImmunizationBatchService) self.mock_table = Mock() - self.controller = ImmunizationBatchController( - immunization_repo=self.mock_repo, - fhir_service=self.mock_service - ) + self.controller = ImmunizationBatchController(immunization_repo=self.mock_repo, fhir_service=self.mock_service) def test_send_request_to_dynamo_create_success(self): """it should create Immunization and return imms id location""" @@ -32,7 +31,7 @@ def test_send_request_to_dynamo_create_success(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } self.mock_service.create_immunization.return_value = imms_id @@ -41,26 +40,27 @@ def test_send_request_to_dynamo_create_success(self): self.assertEqual(result, imms_id) self.mock_service.create_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) - def test_send_request_to_dynamo_create_badrequest(self): """it should return error since it got failed in initial validation""" imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - create_result = CustomValidationError(message = "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists") + create_result = CustomValidationError( + message="Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists" + ) message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } self.mock_service.create_immunization.return_value = create_result @@ -69,11 +69,11 @@ def test_send_request_to_dynamo_create_badrequest(self): self.assertEqual(result, create_result) self.mock_service.create_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_create_duplicate(self): @@ -86,7 +86,7 @@ def test_send_request_to_dynamo_create_duplicate(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } self.mock_service.create_immunization.return_value = create_result @@ -95,11 +95,11 @@ def test_send_request_to_dynamo_create_duplicate(self): self.assertEqual(result, create_result) self.mock_service.create_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_create_unhandled_error(self): @@ -107,37 +107,37 @@ def test_send_request_to_dynamo_create_unhandled_error(self): imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - update_result = UnhandledResponseError(response='Non-200 response from dynamodb', message='connection timeout') + update_result = UnhandledResponseError(response="Non-200 response from dynamodb", message="connection timeout") message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "CREATE" + "operation_requested": "CREATE", } - self.mock_service.create_immunization.return_value = UnhandledResponseError("Non-200 response from dynamodb", "connection timeout") + self.mock_service.create_immunization.return_value = UnhandledResponseError( + "Non-200 response from dynamodb", "connection timeout" + ) result = self.controller.send_request_to_dynamo(message_body, self.mock_table, True) self.assertEqual(result, update_result) self.mock_service.create_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) + class TestUpdateImmunizationBatchController(unittest.TestCase): def setUp(self): self.mock_repo = create_autospec(ImmunizationBatchRepository) self.mock_service = create_autospec(ImmunizationBatchService) self.mock_table = Mock() - self.controller = ImmunizationBatchController( - immunization_repo=self.mock_repo, - fhir_service=self.mock_service - ) + self.controller = ImmunizationBatchController(immunization_repo=self.mock_repo, fhir_service=self.mock_service) def test_send_request_to_dynamo_update_success(self): """it should update Immunization and return imms id""" @@ -148,7 +148,7 @@ def test_send_request_to_dynamo_update_success(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "UPDATE" + "operation_requested": "UPDATE", } self.mock_service.update_immunization.return_value = imms_id @@ -157,11 +157,11 @@ def test_send_request_to_dynamo_update_success(self): self.assertEqual(result, imms_id) self.mock_service.update_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_update_badrequest(self): @@ -169,12 +169,14 @@ def test_send_request_to_dynamo_update_badrequest(self): imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - update_result = CustomValidationError(message = "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists") + update_result = CustomValidationError( + message="Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists" + ) message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "UPDATE" + "operation_requested": "UPDATE", } self.mock_service.update_immunization.return_value = update_result @@ -183,11 +185,11 @@ def test_send_request_to_dynamo_update_badrequest(self): self.assertEqual(result, update_result) self.mock_service.update_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_update_resource_not_found(self): @@ -200,7 +202,7 @@ def test_send_request_to_dynamo_update_resource_not_found(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "UPDATE" + "operation_requested": "UPDATE", } self.mock_service.update_immunization.return_value = update_result @@ -209,11 +211,11 @@ def test_send_request_to_dynamo_update_resource_not_found(self): self.assertEqual(result, update_result) self.mock_service.update_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_update_unhandled_error(self): @@ -221,25 +223,27 @@ def test_send_request_to_dynamo_update_unhandled_error(self): imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - update_result = UnhandledResponseError(response='Non-200 response from dynamodb', message='connection timeout') + update_result = UnhandledResponseError(response="Non-200 response from dynamodb", message="connection timeout") message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "UPDATE" + "operation_requested": "UPDATE", } - self.mock_service.update_immunization.return_value = UnhandledResponseError("Non-200 response from dynamodb", "connection timeout") + self.mock_service.update_immunization.return_value = UnhandledResponseError( + "Non-200 response from dynamodb", "connection timeout" + ) result = self.controller.send_request_to_dynamo(message_body, self.mock_table, True) self.assertEqual(result, update_result) self.mock_service.update_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) @@ -249,10 +253,7 @@ def setUp(self): self.mock_repo = create_autospec(ImmunizationBatchRepository) self.mock_service = create_autospec(ImmunizationBatchService) self.mock_table = Mock() - self.controller = ImmunizationBatchController( - immunization_repo=self.mock_repo, - fhir_service=self.mock_service - ) + self.controller = ImmunizationBatchController(immunization_repo=self.mock_repo, fhir_service=self.mock_service) def test_send_request_to_dynamo_delete_success(self): """it should delete Immunization and return imms id""" @@ -263,7 +264,7 @@ def test_send_request_to_dynamo_delete_success(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "DELETE" + "operation_requested": "DELETE", } self.mock_service.delete_immunization.return_value = imms_id @@ -272,11 +273,11 @@ def test_send_request_to_dynamo_delete_success(self): self.assertEqual(result, imms_id) self.mock_service.delete_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_delete_badrequest(self): @@ -284,12 +285,14 @@ def test_send_request_to_dynamo_delete_badrequest(self): imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - update_result = CustomValidationError(message = "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists") + update_result = CustomValidationError( + message="Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists" + ) message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "DELETE" + "operation_requested": "DELETE", } self.mock_service.delete_immunization.return_value = update_result @@ -298,11 +301,11 @@ def test_send_request_to_dynamo_delete_badrequest(self): self.assertEqual(result, update_result) self.mock_service.delete_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_delete_resource_not_found(self): @@ -315,7 +318,7 @@ def test_send_request_to_dynamo_delete_resource_not_found(self): "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "DELETE" + "operation_requested": "DELETE", } self.mock_service.delete_immunization.return_value = update_result @@ -324,11 +327,11 @@ def test_send_request_to_dynamo_delete_resource_not_found(self): self.assertEqual(result, update_result) self.mock_service.delete_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) def test_send_request_to_dynamo_delete_unhandled_error(self): @@ -336,27 +339,29 @@ def test_send_request_to_dynamo_delete_unhandled_error(self): imms_id = str(uuid.uuid4()) imms = create_covid_19_immunization(imms_id) - update_result = UnhandledResponseError(response='Non-200 response from dynamodb', message='connection timeout') + update_result = UnhandledResponseError(response="Non-200 response from dynamodb", message="connection timeout") message_body = { "supplier": "test_supplier", "fhir_json": imms.json(), "vax_type": "test_vax", - "operation_requested": "DELETE" + "operation_requested": "DELETE", } - self.mock_service.delete_immunization.return_value = UnhandledResponseError("Non-200 response from dynamodb", "connection timeout") + self.mock_service.delete_immunization.return_value = UnhandledResponseError( + "Non-200 response from dynamodb", "connection timeout" + ) result = self.controller.send_request_to_dynamo(message_body, self.mock_table, True) self.assertEqual(result, update_result) self.mock_service.delete_immunization.assert_called_once_with( - immunization=message_body['fhir_json'], - supplier_system=message_body['supplier'], - vax_type=message_body['vax_type'], + immunization=message_body["fhir_json"], + supplier_system=message_body["supplier"], + vax_type=message_body["vax_type"], table=self.mock_table, - is_present=True + is_present=True, ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/backend/tests/controller/test_fhir_controller.py b/backend/tests/controller/test_fhir_controller.py index caad9683b..8c298faa2 100644 --- a/backend/tests/controller/test_fhir_controller.py +++ b/backend/tests/controller/test_fhir_controller.py @@ -1,20 +1,17 @@ import base64 -import urllib - import json import unittest +import urllib +import urllib.parse import uuid +from unittest.mock import create_autospec, ANY, patch, Mock +from urllib.parse import urlencode from fhir.resources.R4B.bundle import Bundle from fhir.resources.R4B.immunization import Immunization -from unittest.mock import create_autospec, ANY, patch, Mock -from urllib.parse import urlencode -import urllib.parse from controller.aws_apig_response_utils import create_response from controller.fhir_controller import FhirController -from repository.fhir_repository import ImmunizationRepository -from service.fhir_service import FhirService, UpdateOutcome from models.errors import ( ResourceNotFoundError, UnhandledResponseError, @@ -24,9 +21,12 @@ UnauthorizedVaxError, IdentifierDuplicationError, ) -from testing_utils.immunization_utils import create_covid_19_immunization from parameter_parser import patient_identifier_system, process_search_params +from repository.fhir_repository import ImmunizationRepository +from service.fhir_service import FhirService, UpdateOutcome from testing_utils.generic_utils import load_json_data +from testing_utils.immunization_utils import create_covid_19_immunization + class TestFhirControllerBase(unittest.TestCase): """Base class for all tests to set up common fixtures""" @@ -43,6 +43,7 @@ def tearDown(self): self.logger_info_patcher.stop() super().tearDown() + class TestFhirController(TestFhirControllerBase): def setUp(self): super().setUp() @@ -50,7 +51,6 @@ def setUp(self): self.repository = create_autospec(ImmunizationRepository) self.controller = FhirController(self.service) - def test_create_response(self): """it should return application/fhir+json with correct status code""" body = {"message": "a body"} @@ -86,7 +86,10 @@ def tearDown(self): def test_get_imms_by_identifer(self): """it should return Immunization Id if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { "headers": {"SupplierSystem": "test"}, "queryStringParameters": { @@ -102,9 +105,7 @@ def test_get_imms_by_identifer(self): # When response = self.controller.get_immunization_by_identifier(lambda_event) # Then - self.service.get_immunization_by_identifier.assert_called_once_with( - identifiers, "test", identifier, _element - ) + self.service.get_immunization_by_identifier.assert_called_once_with(identifiers, "test", identifier, _element) self.assertEqual(response["statusCode"], 200) body = json.loads(response["body"]) @@ -156,9 +157,7 @@ def test_not_found_for_identifier(self): response = self.controller.get_immunization_by_identifier(lambda_event) # Then - self.service.get_immunization_by_identifier.assert_called_once_with( - imms, "test", identifier, _element - ) + self.service.get_immunization_by_identifier.assert_called_once_with(imms, "test", identifier, _element) self.assertEqual(response["statusCode"], 200) body = json.loads(response["body"]) @@ -169,10 +168,16 @@ def test_not_found_for_identifier(self): def test_get_imms_by_identifer_patient_identifier_and_element_present(self): """it should return Immunization Id if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { "headers": {"SupplierSystem": "test"}, - "queryStringParameters": {"patient.identifier": "test", "_elements": "id,meta"}, + "queryStringParameters": { + "patient.identifier": "test", + "_elements": "id,meta", + }, "body": None, } # When @@ -187,7 +192,10 @@ def test_get_imms_by_identifer_patient_identifier_and_element_present(self): def test_get_imms_by_identifer_both_body_and_query_params_present(self): """it should return Immunization Id if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { "headers": {"SupplierSystem": "test"}, "queryStringParameters": { @@ -209,9 +217,12 @@ def test_get_imms_by_identifer_both_body_and_query_params_present(self): def test_get_imms_by_identifer_both_identifier_present(self): """it should return Immunization Id if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": { "patient.identifier": "test", "identifier": "https://supplierABC/identifiers/vacc|f10b59b3-fc73-4616-99c9-9e882ab31184", @@ -231,9 +242,12 @@ def test_get_imms_by_identifer_both_identifier_present(self): def test_get_imms_by_identifer_invalid_element(self): """it should return 400 as it contain invalid _element if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": { "identifier": "https://supplierABC/identifiers/vacc|f10b59b3-fc73-4616-99c9-9e882ab31184", "_elements": "id,meta,name", @@ -258,7 +272,12 @@ def test_validate_immunization_identifier_is_empty(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "The provided identifiervalue is either missing or not in the expected format.", } @@ -287,7 +306,12 @@ def test_validate_immunization_identifier_in_invalid_format(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "identifier must be in the format of identifier.system|identifier.value e.g. http://pinnacle.org/vaccs|2345-gh3s-r53h7-12ny", } @@ -319,14 +343,19 @@ def test_validate_immunization_identifier_having_whitespace(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "The provided identifiervalue is either missing or not in the expected format.", } ], } lambda_event = { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": { "identifier": "https://supplierABC/identifiers/vacc | f10b59b3-fc73-4616-99c9-9e882ab31184", "_elements": "id", @@ -359,9 +388,7 @@ def test_validate_imms_id_invalid_vaccinetype(self): response = self.controller.get_immunization_by_identifier(lambda_event) # Then - self.service.get_immunization_by_identifier.assert_called_once_with( - identifiers, "test", identifier, _element - ) + self.service.get_immunization_by_identifier.assert_called_once_with(identifiers, "test", identifier, _element) self.assertEqual(response["statusCode"], 403) body = json.loads(response["body"]) @@ -377,7 +404,7 @@ def setUp(self): def set_up_lambda_event(self, body): """Helper to create and set up a lambda event with the given body""" return { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": None, "body": "aWRlbnRpZmllcj1odHRwcyUzQSUyRiUyRnN1cHBsaWVyQUJDJTJGaWRlbnRpZmllcnMlMkZ2YWNjJTdDZjEwYjU5YjMtZmM3My00NjE2LTk5YzktOWU4ODJhYjMxMTg0Jl9lbGVtZW50cz1pZCUyQ21ldGEmaWQ9cw==", } @@ -396,7 +423,10 @@ def parse_lambda_body(self, lambda_event): def test_get_imms_by_identifier(self): """It should return Immunization Id if it exists""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } body = "identifier=https://supplierABC/identifiers/vacc#f10b59b3-fc73-4616-99c9-9e882ab31184&_elements=id|meta" lambda_event = self.set_up_lambda_event(body) identifiers, converted_identifier, converted_element = self.parse_lambda_body(lambda_event) @@ -447,7 +477,10 @@ def test_not_found_for_identifier(self): def test_get_imms_by_identifer_patient_identifier_and_element_present(self): """it should return 400 as its having invalid request""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { "headers": {"SupplierSystem": "test"}, "queryStringParameters": None, @@ -465,9 +498,12 @@ def test_get_imms_by_identifer_patient_identifier_and_element_present(self): def test_get_imms_by_identifer_imms_identifier_and_element_not_present(self): """it should return 400 as its having invalid request""" # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": None, "body": "aWRlbnRpZmllcj1odHRwcyUzQSUyRiUyRnN1cHBsaWVyQUJDJTJGaWRlbnRpZmllcnMlMkZ2YWNjJSAgN0NmMTBiNTliMy1mYzczLTQ2MTYtOTljOS05ZTg4MmFiMzExODQmX2VsZW1lbnRzPWlkJTJDbWV0YSZpZD1z", } @@ -491,14 +527,19 @@ def test_validate_immunization_element_is_empty(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "The provided identifiervalue is either missing or not in the expected format.", } ], } lambda_event = { - "headers": { "SupplierSystem": "test"}, + "headers": {"SupplierSystem": "test"}, "queryStringParameters": None, "body": "aW1tdW5pemF0aW9uLmlkZW50aWZpZXI9aHR0cHMlM0ElMkYlMkZzdXBwbGllckFCQyUyRmlkZW50aWZpZXJzJTJGdmFjYyU3Q2YxMGI1OWIzLWZjNzMtNDYxNi05OWM5LTllODgyYWIzMTE4NCZfZWxlbWVudD0nJw==", } @@ -521,7 +562,12 @@ def test_validate_immunization_identifier_is_invalid(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "The provided identifiervalue is either missing or not in the expected format.", } @@ -543,7 +589,10 @@ def test_get_imms_by_identifer_both_identifier_present(self): """it should return 400 as its having invalid request""" # Given # Given - self.service.get_immunization_by_identifier.return_value = {"id": "test", "Version": 1} + self.service.get_immunization_by_identifier.return_value = { + "id": "test", + "Version": 1, + } lambda_event = { "headers": {"SupplierSystem": "test"}, "queryStringParameters": None, @@ -584,7 +633,12 @@ def test_validate_immunization_identifier_is_empty(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "identifier must be in the format of identifier.system|identifier.value e.g. http://pinnacle.org/vaccs|2345-gh3s-r53h7-12ny", } @@ -613,7 +667,12 @@ def test_validate_immunization_identifier_having_whitespace(self): "severity": "error", "code": "invalid", "details": { - "coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}] + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID", + } + ] }, "diagnostics": "The provided identifiervalue is either missing or not in the expected format.", } @@ -673,7 +732,10 @@ def test_get_imms_by_id(self): """it should return Immunization resource if it exists""" # Given imms_id = "a-id" - self.service.get_immunization_and_version_by_id.return_value = (Immunization.construct(), "1") + self.service.get_immunization_and_version_by_id.return_value = ( + Immunization.construct(), + "1", + ) lambda_event = { "headers": {"SupplierSystem": "test"}, "pathParameters": {"id": imms_id}, @@ -731,8 +793,7 @@ def test_not_found(self): # Given imms_id = "a-non-existing-id" self.service.get_immunization_and_version_by_id.side_effect = ResourceNotFoundError( - resource_type="Immunization", - resource_id=imms_id + resource_type="Immunization", resource_id=imms_id ) lambda_event = { "headers": {"SupplierSystem": "test"}, @@ -902,11 +963,15 @@ def test_update_immunization(self): imms_id = "valid-id" imms = '{"id": "valid-id"}' aws_event = { - "headers": {"E-Tag": 1,"SupplierSystem": "Test"}, + "headers": {"E-Tag": 1, "SupplierSystem": "Test"}, "body": imms, "pathParameters": {"id": imms_id}, } - self.service.update_immunization.return_value = UpdateOutcome.UPDATE, "value doesn't matter", 2 + self.service.update_immunization.return_value = ( + UpdateOutcome.UPDATE, + "value doesn't matter", + 2, + ) self.service.get_immunization_by_id_all.return_value = { "resource": "new_value", "Version": 1, @@ -916,9 +981,7 @@ def test_update_immunization(self): } response = self.controller.update_immunization(aws_event) - self.service.update_immunization.assert_called_once_with( - imms_id, json.loads(imms), 1, "COVID19", "Test" - ) + self.service.update_immunization.assert_called_once_with(imms_id, json.loads(imms), 1, "COVID19", "Test") self.assertEqual(response["statusCode"], 200) self.assertEqual(response["headers"]["E-Tag"], 2) @@ -928,16 +991,13 @@ def test_update_immunization_etag_missing(self): imms_id = "valid-id" imms = {"id": "valid-id"} self.service.get_immunization_by_id_all.return_value = { - "id": imms_id, - "Version": 1, - "VaccineType": "COVID19", - "DeletedAt": False - } + "id": imms_id, + "Version": 1, + "VaccineType": "COVID19", + "DeletedAt": False, + } aws_event = { - "headers": { - "SupplierSystem": "Test", - "operation_requested": "update" - }, + "headers": {"SupplierSystem": "Test", "operation_requested": "update"}, "body": json.dumps(imms), "pathParameters": {"id": imms_id}, } @@ -957,7 +1017,7 @@ def test_update_immunization_duplicate(self): "headers": { "E-Tag": 1, "SupplierSystem": "Test", - "operation_requested": "update" + "operation_requested": "update", }, "body": json.dumps(imms), "pathParameters": {"id": imms_id}, @@ -981,7 +1041,7 @@ def test_update_immunization_UnauthorizedVaxError(self): "headers": { "E-Tag": 1, "SupplierSystem": "Test", - "operation_requested": "update" + "operation_requested": "update", }, "body": json.dumps(imms), "pathParameters": {"id": imms_id}, @@ -1006,12 +1066,15 @@ def test_update_immunization_for_batch_existing_record_is_none(self): "headers": { "E-Tag": 1, "SupplierSystem": "Test", - "operation_requested": "update" + "operation_requested": "update", }, "body": json.dumps(imms), "pathParameters": {"id": imms_id}, } - self.service.update_immunization.return_value = UpdateOutcome.UPDATE, "value doesn't matter" + self.service.update_immunization.return_value = ( + UpdateOutcome.UPDATE, + "value doesn't matter", + ) self.service.get_immunization_by_id_all.return_value = None response = self.controller.update_immunization(aws_event) @@ -1068,9 +1131,7 @@ def test_update_deletedat_immunization_with_version(self): } response = self.controller.update_immunization(aws_event) - self.service.reinstate_immunization.assert_called_once_with( - imms_id, json.loads(imms), 1, "COVID19", "Test" - ) + self.service.reinstate_immunization.assert_called_once_with(imms_id, json.loads(imms), 1, "COVID19", "Test") self.assertEqual(response["statusCode"], 200) self.assertEqual(response["headers"]["E-Tag"], 2) @@ -1080,7 +1141,7 @@ def test_update_deletedat_immunization_without_version(self): imms = '{"id": "valid-id"}' imms_id = "valid-id" aws_event = { - "headers": {"SupplierSystem": "Test", "E-tag":1}, + "headers": {"SupplierSystem": "Test", "E-tag": 1}, "body": imms, "pathParameters": {"id": imms_id}, } @@ -1094,11 +1155,9 @@ def test_update_deletedat_immunization_without_version(self): } response = self.controller.update_immunization(aws_event) - self.service.reinstate_immunization.assert_called_once_with( - imms_id, json.loads(imms), 1, "COVID19", "Test" - ) + self.service.reinstate_immunization.assert_called_once_with(imms_id, json.loads(imms), 1, "COVID19", "Test") self.assertEqual(response["statusCode"], 200) - self.assertEqual(response["headers"]["E-Tag"], 2) + self.assertEqual(response["headers"]["E-Tag"], 2) def test_validation_error(self): """it should return 400 if Immunization is invalid""" @@ -1123,7 +1182,6 @@ def test_validation_error(self): body = json.loads(response["body"]) self.assertEqual(body["resourceType"], "OperationOutcome") - def test_validation_error_for_batch(self): """it should return 400 if Immunization is invalid""" @@ -1132,7 +1190,7 @@ def test_validation_error_for_batch(self): "headers": { "E-Tag": 1, "SupplierSystem": "Test", - "operation_requested": "update" + "operation_requested": "update", }, "body": imms, "pathParameters": {"id": "valid-id"}, @@ -1150,7 +1208,9 @@ def test_validation_error_for_batch(self): body = json.loads(response["body"]) self.assertEqual(body["resourceType"], "OperationOutcome") - def test_validation_superseded_number_to_give_bad_request_for_update_immunization(self): + def test_validation_superseded_number_to_give_bad_request_for_update_immunization( + self, + ): """it should return 400 if Immunization has superseded nhs number.""" update_result = { "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value does not exists" @@ -1183,7 +1243,11 @@ def test_validation_identifier_to_give_bad_request_for_update_immunization(self) req_imms = '{"id": "valid-id"}' path_id = "valid-id" aws_event = { - "headers": {"E-Tag": 1, "VaccineTypePermissions": "COVID19.CRUDS", "SupplierSystem": "Test"}, + "headers": { + "E-Tag": 1, + "VaccineTypePermissions": "COVID19.CRUDS", + "SupplierSystem": "Test", + }, "body": req_imms, "pathParameters": {"id": path_id}, } @@ -1263,7 +1327,11 @@ def test_update_immunization_when_reinstated_true(self): "body": imms, "pathParameters": {"id": imms_id}, } - self.service.update_reinstated_immunization.return_value = UpdateOutcome.UPDATE, {}, 3 + self.service.update_reinstated_immunization.return_value = ( + UpdateOutcome.UPDATE, + {}, + 3, + ) self.service.get_immunization_by_id_all.return_value = { "resource": "existing", "Version": 1, @@ -1307,9 +1375,11 @@ def test_update_reinstated_immunization_with_diagnostics_error(self): "Reinstated": False, "VaccineType": "COVID19", } - self.service.reinstate_immunization.return_value = (None, { - "diagnostics": "Patient NHS number has been superseded" - }, None) + self.service.reinstate_immunization.return_value = ( + None, + {"diagnostics": "Patient NHS number has been superseded"}, + None, + ) response = self.controller.update_immunization(aws_event) @@ -1329,7 +1399,10 @@ def tearDown(self): def test_validate_imms_id(self): """it should validate lambda's Immunization id""" - invalid_id = {"pathParameters": {"id": "invalid %$ id"}, "headers": {"SupplierSystem": "Test"}} + invalid_id = { + "pathParameters": {"id": "invalid %$ id"}, + "headers": {"SupplierSystem": "Test"}, + } response = self.controller.delete_immunization(invalid_id) @@ -1368,10 +1441,7 @@ def test_delete_immunization_unauthorised_vax(self): imms_id = "an-id" self.service.delete_immunization.side_effect = UnauthorizedVaxError() lambda_event = { - "headers": { - "SupplierSystem": "Test", - "operation_requested": "delete" - }, + "headers": {"SupplierSystem": "Test", "operation_requested": "delete"}, "pathParameters": {"id": imms_id}, } @@ -1420,11 +1490,22 @@ def test_immunization_unhandled_error(self): self.assertEqual(body["resourceType"], "OperationOutcome") self.assertEqual(body["issue"][0]["code"], "exception") + class TestSearchImmunizations(TestFhirControllerBase): MOCK_REDIS_V2D_HKEYS = { - "PERTUSSIS", "RSV", "3in1", "MMR", "HPV", "MMRV", "PCV13", - "SHINGLES", "COVID19", "FLU", "MENACWY" + "PERTUSSIS", + "RSV", + "3in1", + "MMR", + "HPV", + "MMRV", + "PCV13", + "SHINGLES", + "COVID19", + "FLU", + "MENACWY", } + def setUp(self): super().setUp() self.service = create_autospec(FhirService) @@ -1444,7 +1525,12 @@ def test_get_search_immunizations(self): vaccine_type = "COVID19" params = f"{self.immunization_target_key}={vaccine_type}&" + urllib.parse.urlencode( - [(f"{self.patient_identifier_key}", f"{self.patient_identifier_valid_value}")] + [ + ( + f"{self.patient_identifier_key}", + f"{self.patient_identifier_valid_value}", + ) + ] ) lambda_event = { "headers": { @@ -1489,7 +1575,9 @@ def test_get_search_immunizations_vax_permission_check(self): body = json.loads(response["body"]) self.assertEqual(body["resourceType"], "OperationOutcome") - def test_get_search_immunizations_for_unauthorized_vaccine_type_search(self,): + def test_get_search_immunizations_for_unauthorized_vaccine_type_search( + self, + ): """it should return 200 and contains warning operation outcome as the user is not having authorization for one of the vaccine type""" search_result = load_json_data("sample_immunization_response _for _not_done_event.json") bundle = Bundle.parse_obj(search_result) @@ -1499,7 +1587,10 @@ def test_get_search_immunizations_for_unauthorized_vaccine_type_search(self,): vaccine_type = ",".join(vaccine_type) lambda_event = { - "headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "test",}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "SupplierSystem": "test", + }, "multiValueQueryStringParameters": { self.immunization_target_key: [vaccine_type], self.patient_identifier_key: [self.patient_identifier_valid_value], @@ -1515,7 +1606,10 @@ def test_get_search_immunizations_for_unauthorized_vaccine_type_search(self,): operation_outcome_present = any( entry["resource"]["resourceType"] == "OperationOutcome" for entry in body.get("entry", []) ) - self.assertTrue(operation_outcome_present, "OperationOutcome resource is not present in the response") + self.assertTrue( + operation_outcome_present, + "OperationOutcome resource is not present in the response", + ) def test_get_search_immunizations_for_unauthorized_vaccine_type_search_400(self): """it should return 400 as the request has an invalid vaccine type""" @@ -1526,7 +1620,10 @@ def test_get_search_immunizations_for_unauthorized_vaccine_type_search_400(self) vaccine_type = "FLUE" lambda_event = { - "headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "test"}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "SupplierSystem": "test", + }, "multiValueQueryStringParameters": { self.immunization_target_key: [vaccine_type], self.patient_identifier_key: [self.patient_identifier_valid_value], @@ -1546,7 +1643,12 @@ def test_post_search_immunizations(self): vaccine_type = "COVID19" params = f"{self.immunization_target_key}={vaccine_type}&" + urllib.parse.urlencode( - [(f"{self.patient_identifier_key}", f"{self.patient_identifier_valid_value}")] + [ + ( + f"{self.patient_identifier_key}", + f"{self.patient_identifier_valid_value}", + ) + ] ) # Construct the application/x-www-form-urlencoded body body = { @@ -1560,7 +1662,10 @@ def test_post_search_immunizations(self): # Construct the lambda event lambda_event = { "httpMethod": "POST", - "headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "Test"}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "SupplierSystem": "Test", + }, "body": base64_encoded_body, } # When @@ -1593,7 +1698,10 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search(self): # Construct the lambda event lambda_event = { "httpMethod": "POST", - "headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "Test"}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "SupplierSystem": "Test", + }, "body": base64_encoded_body, } # When @@ -1605,7 +1713,10 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search(self): operation_outcome_present = any( entry["resource"]["resourceType"] == "OperationOutcome" for entry in body.get("entry", []) ) - self.assertTrue(operation_outcome_present, "OperationOutcome resource is not present in the response") + self.assertTrue( + operation_outcome_present, + "OperationOutcome resource is not present in the response", + ) def test_post_search_immunizations_for_unauthorized_vaccine_type_search_400(self): """it should return 400 as the request is having invalid vaccine type""" @@ -1627,7 +1738,10 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search_400(self # Construct the lambda event lambda_event = { "httpMethod": "POST", - "headers": {"Content-Type": "application/x-www-form-urlencoded", "VaccineTypePermissions": "flu:search"}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "VaccineTypePermissions": "flu:search", + }, "body": base64_encoded_body, } # When @@ -1638,8 +1752,6 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search_400(self def test_post_search_immunizations_for_unauthorized_vaccine_type_search_403(self): """it should return 403 as the user doesnt have vaccinetype permission""" - search_result = load_json_data("sample_immunization_response _for _not_done_event.json") - bundle = Bundle.parse_obj(search_result) self.service.search_immunizations.side_effect = UnauthorizedVaxError() vaccine_type = ["COVID19", "FLU"] @@ -1657,7 +1769,10 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search_403(self # Construct the lambda event lambda_event = { "httpMethod": "POST", - "headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "Test"}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded", + "SupplierSystem": "Test", + }, "body": base64_encoded_body, } # When @@ -1760,7 +1875,12 @@ def test_self_link_excludes_extraneous_params(self): self.service.search_immunizations.return_value = search_result, False vaccine_type = "COVID19" params = f"{self.immunization_target_key}={vaccine_type}&" + urllib.parse.urlencode( - [(f"{self.patient_identifier_key}", f"{self.patient_identifier_valid_value}")] + [ + ( + f"{self.patient_identifier_key}", + f"{self.patient_identifier_valid_value}", + ) + ] ) lambda_event = { diff --git a/backend/tests/models/utils/test_generic_utils.py b/backend/tests/models/utils/test_generic_utils.py index 852bc8598..f02d09f41 100644 --- a/backend/tests/models/utils/test_generic_utils.py +++ b/backend/tests/models/utils/test_generic_utils.py @@ -1,11 +1,11 @@ """Generic utils for tests""" import unittest +from datetime import datetime, date + from src.models.utils.generic_utils import form_json -from testing_utils.generic_utils import load_json_data, format_date_types -import unittest -from datetime import datetime, date +from testing_utils.generic_utils import load_json_data, format_date_types class TestFormJson(unittest.TestCase): @@ -39,7 +39,10 @@ def test_identifier_with_id_element_truncates_to_id(self): out = form_json(self.response, "id", self.identifier, self.baseurl) res = out["entry"][0]["resource"] self.assertEqual(out["total"], 1) - self.assertEqual(out["link"][0]["url"], f"{self.baseurl}?identifier={self.identifier}&_elements=id") + self.assertEqual( + out["link"][0]["url"], + f"{self.baseurl}?identifier={self.identifier}&_elements=id", + ) self.assertEqual(res["resourceType"], "Immunization") self.assertEqual(res["id"], self.response["id"]) self.assertNotIn("meta", res) @@ -48,7 +51,10 @@ def test_identifier_with_meta_element_truncates_to_meta(self): out = form_json(self.response, "meta", self.identifier, self.baseurl) res = out["entry"][0]["resource"] self.assertEqual(out["total"], 1) - self.assertEqual(out["link"][0]["url"], f"{self.baseurl}?identifier={self.identifier}&_elements=meta") + self.assertEqual( + out["link"][0]["url"], + f"{self.baseurl}?identifier={self.identifier}&_elements=meta", + ) self.assertEqual(res["resourceType"], "Immunization") self.assertIn("meta", res) self.assertEqual(res["meta"]["versionId"], self.response["version"]) @@ -57,7 +63,10 @@ def test_identifier_with_id_and_meta_elements_truncates_both(self): out = form_json(self.response, "id,meta", self.identifier, self.baseurl) res = out["entry"][0]["resource"] self.assertEqual(out["total"], 1) - self.assertEqual(out["link"][0]["url"], f"{self.baseurl}?identifier={self.identifier}&_elements=id,meta") + self.assertEqual( + out["link"][0]["url"], + f"{self.baseurl}?identifier={self.identifier}&_elements=id,meta", + ) self.assertEqual(res["resourceType"], "Immunization") self.assertEqual(res["id"], self.response["id"]) self.assertIn("meta", res) @@ -69,7 +78,7 @@ def test_elements_whitespace_and_case_are_handled(self): res = out["entry"][0]["resource"] self.assertEqual( out["link"][0]["url"], - f"{self.baseurl}?identifier={self.identifier}&_elements={raw_elements}" + f"{self.baseurl}?identifier={self.identifier}&_elements={raw_elements}", ) self.assertEqual(res["id"], self.response["id"]) self.assertEqual(res["meta"]["versionId"], self.response["version"]) diff --git a/backend/tests/repository/test_fhir_batch_repository.py b/backend/tests/repository/test_fhir_batch_repository.py index e3a64c2a9..ee7ff89f8 100644 --- a/backend/tests/repository/test_fhir_batch_repository.py +++ b/backend/tests/repository/test_fhir_batch_repository.py @@ -1,12 +1,19 @@ import os import unittest from unittest.mock import MagicMock, ANY, patch +from uuid import uuid4 + import boto3 -import simplejson as json import botocore.exceptions +import simplejson as json from moto import mock_aws -from uuid import uuid4 -from models.errors import IdentifierDuplicationError, ResourceNotFoundError, UnhandledResponseError, ResourceFoundError + +from models.errors import ( + IdentifierDuplicationError, + ResourceNotFoundError, + UnhandledResponseError, + ResourceFoundError, +) from repository.fhir_batch_repository import ImmunizationBatchRepository, create_table from testing_utils.immunization_utils import create_covid_19_immunization_dict @@ -16,6 +23,7 @@ def _make_immunization_pk(_id): return f"Immunization#{_id}" + @mock_aws class TestImmunizationBatchRepository(unittest.TestCase): @@ -28,7 +36,7 @@ def setUp(self): self.table.put_item = MagicMock(return_value={"ResponseMetadata": {"HTTPStatusCode": 200}}) self.table.query = MagicMock(return_value={}) self.immunization = create_covid_19_immunization_dict(imms_id) - self.table.update_item = MagicMock(return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}) + self.table.update_item = MagicMock(return_value={"ResponseMetadata": {"HTTPStatusCode": 200}}) self.redis_patcher = patch("models.utils.validation_utils.redis_client") self.mock_redis_client = self.redis_patcher.start() self.logger_info_patcher = patch("logging.Logger.info") @@ -37,6 +45,7 @@ def setUp(self): def tearDown(self): patch.stopall() + class TestCreateImmunization(TestImmunizationBatchRepository): def modify_immunization(self, remove_nhs): @@ -49,12 +58,10 @@ def modify_immunization(self, remove_nhs): def create_immunization_test_logic(self, is_present, remove_nhs): """Common logic for testing immunization creation.""" - self.mock_redis_client.hget.side_effect = ['COVID19'] + self.mock_redis_client.hget.side_effect = ["COVID19"] self.modify_immunization(remove_nhs) - self.repository.create_immunization( - self.immunization, "supplier", "vax-type", self.table, is_present - ) + self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, is_present) item = self.table.put_item.call_args.kwargs["Item"] self.table.put_item.assert_called_with( @@ -68,7 +75,7 @@ def create_immunization_test_logic(self, is_present, remove_nhs): "Version": 1, "SupplierSystem": "supplier", }, - ConditionExpression=ANY + ConditionExpression=ANY, ) self.assertEqual(item["PK"], f'Immunization#{self.immunization["id"]}') @@ -81,15 +88,16 @@ def test_create_immunization_without_nhs_number(self): self.create_immunization_test_logic(is_present=False, remove_nhs=True) - def test_create_immunization_duplicate(self): """it should not create Immunization since the request is duplicate""" - self.table.query = MagicMock(return_value={ - "id": imms_id, - "identifier": [{"system": "test-system", "value": "12345"}], - "contained": [{"resourceType": "Patient", "identifier": [{"value": "98765"}]}], - "Count": 1 - }) + self.table.query = MagicMock( + return_value={ + "id": imms_id, + "identifier": [{"system": "test-system", "value": "12345"}], + "contained": [{"resourceType": "Patient", "identifier": [{"value": "98765"}]}], + "Count": 1, + } + ) with self.assertRaises(IdentifierDuplicationError): self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.table.put_item.assert_not_called() @@ -104,12 +112,15 @@ def test_create_should_catch_dynamo_error(self): self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) - def test_create_immunization_unhandled_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - response = {'Error': {'Code': 'InternalServerError'}} - with unittest.mock.patch.object(self.table, 'put_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "PutItem")): + response = {"Error": {"Code": "InternalServerError"}} + with unittest.mock.patch.object( + self.table, + "put_item", + side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "PutItem"), + ): with self.assertRaises(UnhandledResponseError) as e: self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) @@ -117,7 +128,13 @@ def test_create_immunization_unhandled_error(self): def test_create_immunization_conditionalcheckfailedexception_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - with unittest.mock.patch.object(self.table, 'put_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "PutItem")): + with unittest.mock.patch.object( + self.table, + "put_item", + side_effect=botocore.exceptions.ClientError( + {"Error": {"Code": "ConditionalCheckFailedException"}}, "PutItem" + ), + ): with self.assertRaises(ResourceFoundError): self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False) @@ -131,47 +148,57 @@ def test_update_immunization(self): { "query_response": { "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1 - }] + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + } + ], }, - "expected_extra_values": {} # No extra assertion values + "expected_extra_values": {}, # No extra assertion values }, # Reinstated scenario { "query_response": { "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1, - "DeletedAt": "20210101" - }] + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + "DeletedAt": "20210101", + } + ], }, - "expected_extra_values": {":respawn": "reinstated"} + "expected_extra_values": {":respawn": "reinstated"}, }, # Update reinstated scenario { "query_response": { "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1, - "DeletedAt": "reinstated" - }] + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + "DeletedAt": "reinstated", + } + ], }, - "expected_extra_values": {} - } + "expected_extra_values": {}, + }, ] for is_present in [True, False]: for case in test_cases: with self.subTest(is_present=is_present, case=case): self.table.query = MagicMock(return_value=case["query_response"]) response = self.repository.update_immunization( - self.immunization, "supplier", "vax-type", self.table, is_present + self.immunization, + "supplier", + "vax-type", + self.table, + is_present, ) expected_values = { ":timestamp": ANY, @@ -180,7 +207,7 @@ def test_update_immunization(self): ":imms_resource_val": json.dumps(self.immunization), ":operation": "UPDATE", ":version": 2, - ":supplier_system": "supplier" + ":supplier_system": "supplier", } expected_values.update(case["expected_extra_values"]) @@ -207,15 +234,18 @@ def test_update_should_catch_dynamo_error(self): bad_request = 400 response = {"ResponseMetadata": {"HTTPStatusCode": bad_request}} self.table.update_item = MagicMock(return_value=response) - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { "PK": _make_immunization_pk(imms_id), "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + "Version": 1, + } + ], + } + ) with self.assertRaises(UnhandledResponseError) as e: self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) @@ -223,56 +253,82 @@ def test_update_should_catch_dynamo_error(self): def test_update_immunization_unhandled_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - response = {'Error': {'Code': 'InternalServerError'}} - with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem")): + response = {"Error": {"Code": "InternalServerError"}} + with unittest.mock.patch.object( + self.table, + "update_item", + side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem"), + ): with self.assertRaises(UnhandledResponseError) as e: - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + } + ], + } + ) self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) def test_update_immunization_conditionalcheckfailedexception_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem")): - with self.assertRaises(ResourceNotFoundError) as e: - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + with unittest.mock.patch.object( + self.table, + "update_item", + side_effect=botocore.exceptions.ClientError( + {"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem" + ), + ): + with self.assertRaises(ResourceNotFoundError): + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + } + ], + } + ) self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False) + class TestDeleteImmunization(TestImmunizationBatchRepository): def test_delete_immunization(self): """it should delete Immunization record""" - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { "PK": _make_immunization_pk(imms_id), "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + "Version": 1, + } + ], + } + ) for is_present in [True, False]: - response = self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, is_present) + response = self.repository.delete_immunization( + self.immunization, "supplier", "vax-type", self.table, is_present + ) self.table.update_item.assert_called_with( Key={"PK": _make_immunization_pk(imms_id)}, UpdateExpression="SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system", - ExpressionAttributeValues={":timestamp": ANY, ":operation": "DELETE", ":supplier_system": "supplier"}, + ExpressionAttributeValues={ + ":timestamp": ANY, + ":operation": "DELETE", + ":supplier_system": "supplier", + }, ReturnValues=ANY, ConditionExpression=ANY, ) @@ -291,15 +347,18 @@ def test_delete_should_catch_dynamo_error(self): bad_request = 400 response = {"ResponseMetadata": {"HTTPStatusCode": bad_request}} self.table.update_item = MagicMock(return_value=response) - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { "PK": _make_immunization_pk(imms_id), "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + "Version": 1, + } + ], + } + ) with self.assertRaises(UnhandledResponseError) as e: self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) @@ -307,37 +366,54 @@ def test_delete_should_catch_dynamo_error(self): def test_delete_immunization_unhandled_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - response = {'Error': {'Code': 'InternalServerError'}} - with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem")): + response = {"Error": {"Code": "InternalServerError"}} + with unittest.mock.patch.object( + self.table, + "update_item", + side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem"), + ): with self.assertRaises(UnhandledResponseError) as e: - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + } + ], + } + ) self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False) self.assertDictEqual(e.exception.response, response) def test_delete_immunization_conditionalcheckfailedexception_error(self): """it should throw UnhandledResponse when the response from dynamodb can't be handled""" - with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem")): - with self.assertRaises(ResourceNotFoundError) as e: - self.table.query = MagicMock(return_value={ - "Count": 1, - "Items": [{ - "PK": _make_immunization_pk(imms_id), - "Resource": json.dumps(self.immunization), - "Version": 1 - }] - } - ) + with unittest.mock.patch.object( + self.table, + "update_item", + side_effect=botocore.exceptions.ClientError( + {"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem" + ), + ): + with self.assertRaises(ResourceNotFoundError): + self.table.query = MagicMock( + return_value={ + "Count": 1, + "Items": [ + { + "PK": _make_immunization_pk(imms_id), + "Resource": json.dumps(self.immunization), + "Version": 1, + } + ], + } + ) self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False) + @mock_aws @patch.dict(os.environ, {"DYNAMODB_TABLE_NAME": "TestTable"}) class TestCreateTable(TestImmunizationBatchRepository): diff --git a/backend/tests/repository/test_fhir_repository.py b/backend/tests/repository/test_fhir_repository.py index 46e49e419..a09341892 100644 --- a/backend/tests/repository/test_fhir_repository.py +++ b/backend/tests/repository/test_fhir_repository.py @@ -1,21 +1,23 @@ -import simplejson as json import time import unittest import uuid from unittest.mock import MagicMock, patch, ANY import botocore.exceptions +import simplejson as json from boto3.dynamodb.conditions import Attr, Key -from repository.fhir_repository import ImmunizationRepository -from models.utils.validation_utils import get_vaccine_type + from models.errors import ( ResourceNotFoundError, UnhandledResponseError, - IdentifierDuplicationError + IdentifierDuplicationError, ) +from models.utils.validation_utils import get_vaccine_type +from repository.fhir_repository import ImmunizationRepository from testing_utils.generic_utils import update_target_disease_code from testing_utils.immunization_utils import create_covid_19_immunization_dict + def _make_immunization_pk(_id): return f"Immunization#{_id}" @@ -23,6 +25,7 @@ def _make_immunization_pk(_id): def _make_patient_pk(_id): return f"Patient#{_id}" + class TestFhirRepositoryBase(unittest.TestCase): """Base class for all tests to set up common fixtures""" @@ -244,9 +247,7 @@ def test_create_should_catch_dynamo_error(self): with self.assertRaises(UnhandledResponseError) as e: # When - self.repository.create_immunization( - create_covid_19_immunization_dict("an-id"), self.patient, "Test" - ) + self.repository.create_immunization(create_covid_19_immunization_dict("an-id"), self.patient, "Test") # Then self.assertDictEqual(e.exception.response, response) @@ -275,7 +276,6 @@ def setUp(self): self.repository = ImmunizationRepository(table=self.table) self.patient = {"id": "a-patient-id"} - def test_create_patient_gsi(self): """create Immunization method should create Patient index with nhs-number as ID and no system""" imms = create_covid_19_immunization_dict("an-id") @@ -338,9 +338,7 @@ def test_update1(self): mock_time.return_value = now_epoch # When - act_resource, updated_version = self.repository.update_immunization( - imms_id, imms, self.patient, 1, "Test" - ) + act_resource, updated_version = self.repository.update_immunization(imms_id, imms, self.patient, 1, "Test") # Then self.assertDictEqual(act_resource, resource) @@ -422,9 +420,7 @@ def test_reinstate_immunization_success(self): } with patch("time.time", return_value=123456): - result, version = self.repository.reinstate_immunization( - imms_id, imms, self.patient, 1, "Test" - ) + result, version = self.repository.reinstate_immunization(imms_id, imms, self.patient, 1, "Test") self.assertEqual(result, resource) self.assertEqual(version, 2) @@ -443,9 +439,7 @@ def test_update_reinstated_immunization_success(self): } with patch("time.time", return_value=123456): - result, version = self.repository.update_reinstated_immunization( - imms_id, imms, self.patient, 1, "Test" - ) + result, version = self.repository.update_reinstated_immunization(imms_id, imms, self.patient, 1, "Test") self.assertEqual(result, resource) self.assertEqual(version, 2) @@ -483,13 +477,17 @@ def test_delete_immunization(self): with patch("time.time") as mock_time: mock_time.return_value = now_epoch # When - _id = self.repository.delete_immunization(imms_id, "Test") + self.repository.delete_immunization(imms_id, "Test") # Then self.table.update_item.assert_called_once_with( Key={"PK": _make_immunization_pk(imms_id)}, UpdateExpression="SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system", - ExpressionAttributeValues={":timestamp": now_epoch, ":operation": "DELETE", ":supplier_system": "Test"}, + ExpressionAttributeValues={ + ":timestamp": now_epoch, + ":operation": "DELETE", + ":supplier_system": "Test", + }, ReturnValues=ANY, ConditionExpression=ANY, ) @@ -588,12 +586,12 @@ def test_map_results_to_immunizations(self): { "Resource": json.dumps(imms1), "PatientSK": "COVID19#some_other_text", - "Version": "1" + "Version": "1", }, { "Resource": json.dumps(imms2), "PatientSK": "COVID19#some_other_text", - "Version": "1" + "Version": "1", }, ] @@ -629,7 +627,6 @@ def setUp(self): self.repository = ImmunizationRepository(table=self.table) self.patient = {"id": "a-patient-id", "identifier": {"value": "an-identifier"}} - def test_decimal_on_create(self): """it should create Immunization, and preserve decimal value""" imms = create_covid_19_immunization_dict(imms_id="an-id") @@ -678,9 +675,7 @@ def run_update_immunization_test(self, imms_id, imms, resource, updated_dose_qua now_epoch = 123456 with patch("time.time") as mock_time: mock_time.return_value = now_epoch - act_resource, act_version = self.repository.update_immunization( - imms_id, imms, self.patient, 1, "Test" - ) + act_resource, act_version = self.repository.update_immunization(imms_id, imms, self.patient, 1, "Test") self.assertDictEqual(act_resource, resource) self.assertEqual(act_version, 2) diff --git a/backend/tests/sample_data/completed_covid19_immunization_event.json b/backend/tests/sample_data/completed_covid19_immunization_event.json index a3d02e17d..aecae2425 100644 --- a/backend/tests/sample_data/completed_covid19_immunization_event.json +++ b/backend/tests/sample_data/completed_covid19_immunization_event.json @@ -76,7 +76,7 @@ "display": "AstraZeneca Ltd" }, "location": { - "type":"Location", + "type": "Location", "identifier": { "value": "X99999", "system": "https://fhir.nhs.uk/Id/ods-organization-code" diff --git a/backend/tests/sample_data/completed_covid19_immunization_event_for_read.json b/backend/tests/sample_data/completed_covid19_immunization_event_for_read.json index 441416335..aecae2425 100644 --- a/backend/tests/sample_data/completed_covid19_immunization_event_for_read.json +++ b/backend/tests/sample_data/completed_covid19_immunization_event_for_read.json @@ -48,7 +48,8 @@ ] } } - ],"identifier": [ + ], + "identifier": [ { "use": "official", "system": "https://supplierABC/identifiers/vacc", diff --git a/backend/tests/sample_data/completed_flu_immunization_event.json b/backend/tests/sample_data/completed_flu_immunization_event.json index c9be82600..658d31140 100644 --- a/backend/tests/sample_data/completed_flu_immunization_event.json +++ b/backend/tests/sample_data/completed_flu_immunization_event.json @@ -68,15 +68,13 @@ "manufacturer": { "display": "Sanofi" }, - "location": - { - "type":"Location", - "identifier": { + "location": { + "type": "Location", + "identifier": { "value": "X99999", "system": "https://fhir.nhs.uk/Id/ods-organization-code" } - } -, + }, "lotNumber": "41925TJ61", "expirationDate": "2021-07-02", "site": { diff --git a/backend/tests/sample_data/completed_hpv_immunization_event.json b/backend/tests/sample_data/completed_hpv_immunization_event.json index f5539abf8..8bc95f109 100644 --- a/backend/tests/sample_data/completed_hpv_immunization_event.json +++ b/backend/tests/sample_data/completed_hpv_immunization_event.json @@ -69,7 +69,7 @@ "display": "Sanofi" }, "location": { - "type":"Location", + "type": "Location", "identifier": { "value": "X99999", "system": "https://fhir.nhs.uk/Id/ods-organization-code" diff --git a/backend/tests/sample_data/completed_rsv_immunization_event.json b/backend/tests/sample_data/completed_rsv_immunization_event.json index 4a34c0e79..88ae28cbe 100644 --- a/backend/tests/sample_data/completed_rsv_immunization_event.json +++ b/backend/tests/sample_data/completed_rsv_immunization_event.json @@ -75,7 +75,7 @@ "display": "AstraZeneca Ltd" }, "location": { - "type":"Location", + "type": "Location", "identifier": { "value": "X99999", "system": "https://fhir.nhs.uk/Id/ods-organization-code" diff --git a/backend/tests/sample_data/sample_immunization_field_mappings.json b/backend/tests/sample_data/sample_immunization_field_mappings.json deleted file mode 100644 index 743a3ac51..000000000 --- a/backend/tests/sample_data/sample_immunization_field_mappings.json +++ /dev/null @@ -1,281 +0,0 @@ -{ - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "identifier": [ - { - "system": "PERFORMING_PROFESSIONAL_BODY_REG_URI", - "value": "PERFORMING_PROFESSIONAL_BODY_REG_CODE", - } - ], - "name": [ - { - "family": "PERFORMING_PROFESSIONAL_SURNAME", - "given": [ - "PERFORMING_PROFESSIONAL_FORENAME" - ] - } - ] - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "NHS_NUMBER" - } - ], - "name": [ - { - "family": "PERSON_SURNAME", - "given": [ - "PERSON_FORENAME" - ] - } - ], - "gender": "PERSON_GENDER_CODE", - "birthDate": "PERSON_DOB", - "address": [ - { - "postalCode": "PERSON_POSTCODE" - } - ] - }, - { - "resourceType": "QuestionnaireResponse", - "id": "QR1", - "status": "completed", - "item": [ - { - "linkId": "Consent", - "answer": [ - { - "valueCoding": { - "code": "CONSENT_FOR_TREATMENT_CODE", - "display": "CONSENT_FOR_TREATMENT_DESCRIPTION" - } - } - ] - }, - { - "linkId": "CareSetting", - "answer": [ - { - "valueCoding": { - "code": "CARE_SETTING_TYPE_CODE", - "display": "CARE_SETTING_TYPE_DESCRIPTION" - } - } - ] - }, - { - "linkId": "ReduceValidation", - "answer": [ - { - "valueBoolean": REDUCE_VALIDATION_CODE - } - ] - }, - { - "linkId": "ReduceValidationReason", - "answer": [ - { - "valueString": "REDUCE_VALIDATION_REASON" - } - ] - }, - { - "linkId": "LocalPatient", - "answer": [ - { - "valueReference": { - "identifier": { - "system": "LOCAL_PATIENT_URI", - "value": "LOCAL_PATIENT_ID" - } - } - } - ] - }, - { - "linkId": "IpAddress", - "answer": [ - { - "valueString": "IP_ADDRESS" - } - ] - }, - { - "linkId": "UserId", - "answer": [ - { - "valueString": "USER_ID" - } - ] - }, - { - "linkId": "UserName", - "answer": [ - { - "valueString": "USER_NAME" - } - ] - }, - { - "linkId": "SubmittedTimeStamp", - "answer": [ - { - "valueDateTime": "SUBMITTED_TIMESTAMP" - } - ] - }, - { - "linkId": "UserEmail", - "answer": [ - { - "valueString": "USER_EMAIL" - } - ] - }, - { - "linkId": "PerformerSDSJobRole", - "answer": [ - { - "valueString": "SDS_JOB_ROLE_NAME" - } - ] - } - ] - } - ], - "identifier": [ - { - "system": "UNIQUE_ID_URI", - "value": "UNIQUE_ID" - } - ], - "status": "ACTION_FLAG or NOT_GIVEN", - "statusReason": { - "coding": [ - { - "code": "REASON_NOT_GIVEN_CODE", - "display": "REASON_NOT_GIVEN_TERM" - } - ] - }, - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "VACCINE_PRODUCT_CODE", - "display": "VACCINE_PRODUCT_TERM" - } - ] - }, - "patient": { - "reference" : "#Pat1" - }, - "occurrenceDateTime": "DATE_AND_TIME", - "recorded": "RECORDED_DATE", - "primarySource": PRIMARY_SOURCE, - "reportOrigin": { - "text": "REPORT_ORIGIN" - }, - "manufacturer": { - "display": "VACCINATION_MANUFACTURER" - }, - "lotNumber": "BATCH_NUMBER", - "expirationDate": "EXPIRY_DATE", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "SITE_OF_VACCINATION_CODE", - "display": "SITE_OF_VACCINATION_TERM" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "ROUTE_OF_VACCINATION_CODE", - "display": "ROUTE_OF_VACCINATION_TERM" - } - ] - }, - "doseQuantity": { - "value": DOSE_AMOUNT, - "unit": "DOSE_UNIT_TERM", - "system": "http://unitsofmeasure.org", - "code": "DOSE_UNIT_CODE" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "SITE_CODE_TYPE_URI", - "value": "SITE_CODE" - }, - "display": "SITE_NAME" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "INDICATION_CODE", - "display": "INDICATION_TERM" - } - ] - } - ], - "protocolApplied": [ - { - "doseNumberPositiveInt": DOSE_SEQUENCE - } - ], - "location": { - "type": "Location", - "identifier": { - "value": "LOCATION_CODE", - "system": "LOCATION_CODE_TYPE_URI" - } - }, - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "VACCINATION_PROCEDURE_CODE", - "display": "VACCINATION_PROCEDURE_TERM" - } - ] - } - }, - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationSituation", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "VACCINATION_SITUATION_CODE", - "display": "VACCINATION_SITUATION_TERM" - } - ] - } - } - ] - } diff --git a/backend/tests/service/test_fhir_batch_service.py b/backend/tests/service/test_fhir_batch_service.py index 28303ce54..2f4855280 100644 --- a/backend/tests/service/test_fhir_batch_service.py +++ b/backend/tests/service/test_fhir_batch_service.py @@ -2,11 +2,12 @@ import uuid from copy import deepcopy from unittest.mock import Mock, create_autospec, patch -from testing_utils.immunization_utils import create_covid_19_immunization_dict_no_id + from models.errors import CustomValidationError from models.fhir_immunization import ImmunizationValidator from repository.fhir_batch_repository import ImmunizationBatchRepository from service.fhir_batch_service import ImmunizationBatchService +from testing_utils.immunization_utils import create_covid_19_immunization_dict_no_id class TestFhirBatchServiceBase(unittest.TestCase): @@ -55,7 +56,7 @@ def test_create_immunization_valid(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertEqual(result, imms_id) @@ -71,7 +72,7 @@ def test_create_immunization_pre_validation_error(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertTrue(expected_msg in error.exception.message) self.mock_repo.create_immunization.assert_not_called() @@ -90,7 +91,7 @@ def test_create_immunization_post_validation_error(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertTrue(expected_msg in error.exception.message) self.mock_repo.create_immunization.assert_not_called() @@ -126,7 +127,7 @@ def test_update_immunization_valid(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertEqual(result, imms_id) @@ -142,7 +143,7 @@ def test_update_immunization_pre_validation_error(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertTrue(expected_msg in error.exception.message) self.mock_repo.update_immunization.assert_not_called() @@ -162,7 +163,7 @@ def test_update_immunization_post_validation_error(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertTrue(expected_msg in error.exception.message) self.mock_repo.update_immunization.assert_not_called() @@ -189,11 +190,10 @@ def test_delete_immunization_valid(self): supplier_system="test_supplier", vax_type="test_vax", table=self.mock_table, - is_present=True + is_present=True, ) self.assertEqual(result, imms_id) - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/backend/tests/service/test_fhir_service.py b/backend/tests/service/test_fhir_service.py index 255f8c827..66ddebf3a 100644 --- a/backend/tests/service/test_fhir_service.py +++ b/backend/tests/service/test_fhir_service.py @@ -1,33 +1,39 @@ -import json -import uuid import datetime -import unittest +import json import os -from unittest.mock import MagicMock +import unittest +import uuid from copy import deepcopy -from unittest.mock import create_autospec, patch from decimal import Decimal +from unittest.mock import MagicMock +from unittest.mock import create_autospec, patch from fhir.resources.R4B.bundle import Bundle as FhirBundle, BundleEntry from fhir.resources.R4B.immunization import Immunization +from pydantic import ValidationError +from pydantic.error_wrappers import ErrorWrapper from authorisation.api_operation_code import ApiOperationCode from authorisation.authoriser import Authoriser -from repository.fhir_repository import ImmunizationRepository -from service.fhir_service import FhirService, UpdateOutcome, get_service_url -from models.errors import InvalidPatientId, CustomValidationError, UnauthorizedVaxError, ResourceNotFoundError +from constants import NHS_NUMBER_USED_IN_SAMPLE_DATA +from models.errors import ( + InvalidPatientId, + CustomValidationError, + UnauthorizedVaxError, + ResourceNotFoundError, +) from models.fhir_immunization import ImmunizationValidator from models.utils.generic_utils import get_contained_patient -from pydantic import ValidationError -from pydantic.error_wrappers import ErrorWrapper +from repository.fhir_repository import ImmunizationRepository +from service.fhir_service import FhirService, UpdateOutcome, get_service_url +from testing_utils.generic_utils import load_json_data from testing_utils.immunization_utils import ( create_covid_19_immunization, create_covid_19_immunization_dict, create_covid_19_immunization_dict_no_id, VALID_NHS_NUMBER, ) -from testing_utils.generic_utils import load_json_data -from constants import NHS_NUMBER_USED_IN_SAMPLE_DATA + class TestFhirServiceBase(unittest.TestCase): """Base class for all tests to set up common fixtures""" @@ -43,6 +49,7 @@ def tearDown(self): super().tearDown() patch.stopall() + class TestServiceUrl(unittest.TestCase): def setUp(self): @@ -74,6 +81,7 @@ def test_get_service_url(self): url = get_service_url(env, base_path) self.assertEqual(url, f"https://internal-dev.api.service.nhs.uk/{base_path}") + class TestGetImmunizationByAll(TestFhirServiceBase): """Tests for FhirService.get_immunization_by_id""" @@ -170,7 +178,7 @@ def test_post_validation_failed_get_by_all_invalid_target_disease(self): def test_post_validation_failed_get_by_all_missing_patient_name(self): """it should raise CustomValidationError for missing patient name""" - self.mock_redis_client.hget.return_value = 'COVID-19' + self.mock_redis_client.hget.return_value = "COVID-19" valid_imms = create_covid_19_immunization_dict("an-id", VALID_NHS_NUMBER) bad_patient_name_imms = deepcopy(valid_imms) @@ -185,6 +193,7 @@ def test_post_validation_failed_get_by_all_missing_patient_name(self): self.assertTrue(bad_patient_name_msg in error.exception.message) self.imms_repo.get_immunization_by_id_all.assert_not_called() + class TestGetImmunization(TestFhirServiceBase): """Tests for FhirService.get_immunization_by_id""" @@ -205,7 +214,10 @@ def test_get_immunization_by_id(self): imms_id = "an-id" self.mock_redis_client.hget.return_value = "COVID-19" self.authoriser.authorise.return_value = True - self.imms_repo.get_immunization_and_version_by_id.return_value = (create_covid_19_immunization(imms_id).dict(), "") + self.imms_repo.get_immunization_and_version_by_id.return_value = ( + create_covid_19_immunization(imms_id).dict(), + "", + ) # When immunisation, version = self.fhir_service.get_immunization_and_version_by_id(imms_id, "Test Supplier") @@ -228,7 +240,10 @@ def test_immunization_not_found(self): # Then self.imms_repo.get_immunization_and_version_by_id.assert_called_once_with(imms_id) - self.assertEqual("Immunization resource does not exist. ID: non-existent-id", str(error.exception)) + self.assertEqual( + "Immunization resource does not exist. ID: non-existent-id", + str(error.exception), + ) def test_get_immunization_by_id_patient_not_restricted(self): """ @@ -240,7 +255,10 @@ def test_get_immunization_by_id_patient_not_restricted(self): immunization_data = load_json_data("completed_covid19_immunization_event.json") self.mock_redis_client.hget.return_value = "COVID-19" self.authoriser.authorise.return_value = True - self.imms_repo.get_immunization_and_version_by_id.return_value = (immunization_data, "2") + self.imms_repo.get_immunization_and_version_by_id.return_value = ( + immunization_data, + "2", + ) expected_imms = load_json_data("completed_covid19_immunization_event_for_read.json") expected_output = Immunization.parse_obj(expected_imms) @@ -284,7 +302,10 @@ def test_unauthorised_error_raised_when_user_lacks_permissions(self): imms_id = "an-id" self.mock_redis_client.hget.return_value = "COVID-19" self.authoriser.authorise.return_value = False - self.imms_repo.get_immunization_and_version_by_id.return_value = (create_covid_19_immunization(imms_id).dict(), 1) + self.imms_repo.get_immunization_and_version_by_id.return_value = ( + create_covid_19_immunization(imms_id).dict(), + 1, + ) with self.assertRaises(UnauthorizedVaxError): # When @@ -315,9 +336,10 @@ def test_post_validation_failed_get_invalid_target_disease(self): self.assertEqual(bad_target_disease_msg, error.exception.message) self.imms_repo.get_immunization_by_id_all.assert_not_called() + def test_post_validation_failed_get_missing_patient_name(self): """it should raise CustomValidationError for missing patient name on get""" - self.mock_redis_client.hget.return_value = 'COVID-19' + self.mock_redis_client.hget.return_value = "COVID-19" valid_imms = create_covid_19_immunization_dict("an-id", VALID_NHS_NUMBER) bad_patient_name_imms = deepcopy(valid_imms) @@ -332,8 +354,10 @@ def test_post_validation_failed_get_missing_patient_name(self): self.assertTrue(bad_patient_name_msg in error.exception.message) self.imms_repo.get_immunization_by_id_all.assert_not_called() + class TestGetImmunizationIdentifier(unittest.TestCase): """Tests for FhirService.get_immunization_by_id""" + MOCK_SUPPLIER_NAME = "TestSupplier" def setUp(self): @@ -358,12 +382,13 @@ def test_get_immunization_by_identifier(self): self.imms_repo.get_immunization_by_identifier.return_value = { "resource": mock_resource, "id": imms_id, - "version": 1 + "version": 1, }, "covid19" # When - service_resp = self.fhir_service.get_immunization_by_identifier(imms_id, self.MOCK_SUPPLIER_NAME, identifier, - element) + service_resp = self.fhir_service.get_immunization_by_identifier( + imms_id, self.MOCK_SUPPLIER_NAME, identifier, element + ) # Then self.imms_repo.get_immunization_by_identifier.assert_called_once_with(imms_id) @@ -384,7 +409,10 @@ def test_get_immunization_by_identifier_raises_error_when_not_authorised(self): identifier = "test" element = "id,mEta,DDD" self.authoriser.authorise.return_value = False - self.imms_repo.get_immunization_by_identifier.return_value = {"id": "foo", "version": 1}, "covid19" + self.imms_repo.get_immunization_by_identifier.return_value = { + "id": "foo", + "version": 1, + }, "covid19" with self.assertRaises(UnauthorizedVaxError): # When @@ -394,7 +422,6 @@ def test_get_immunization_by_identifier_raises_error_when_not_authorised(self): self.imms_repo.get_immunization_by_identifier.assert_called_once_with(imms) self.authoriser.authorise.assert_called_once_with(self.MOCK_SUPPLIER_NAME, ApiOperationCode.SEARCH, {"covid19"}) - def test_immunization_not_found(self): """it should return None if Immunization doesn't exist""" imms_id = "none" @@ -403,8 +430,9 @@ def test_immunization_not_found(self): self.imms_repo.get_immunization_by_identifier.return_value = None, None # When - act_imms = self.fhir_service.get_immunization_by_identifier(imms_id, self.MOCK_SUPPLIER_NAME, identifier, - element) + act_imms = self.fhir_service.get_immunization_by_identifier( + imms_id, self.MOCK_SUPPLIER_NAME, identifier, element + ) # Then self.imms_repo.get_immunization_by_identifier.assert_called_once_with(imms_id) @@ -431,9 +459,7 @@ def test_create_immunization(self): """it should create Immunization and validate it""" self.mock_redis_client.hget.return_value = "COVID19" self.authoriser.authorise.return_value = True - self.imms_repo.create_immunization.return_value = ( - create_covid_19_immunization_dict_no_id() - ) + self.imms_repo.create_immunization.return_value = create_covid_19_immunization_dict_no_id() nhs_number = VALID_NHS_NUMBER req_imms = create_covid_19_immunization_dict_no_id(nhs_number) @@ -497,14 +523,12 @@ def test_post_validation_failed_create_invalid_target_disease(self): def test_post_validation_failed_create_missing_patient_name(self): """it should raise CustomValidationError for missing patient name on create""" - self.mock_redis_client.hget.return_value = 'COVID-19' + self.mock_redis_client.hget.return_value = "COVID-19" valid_imms = create_covid_19_immunization_dict_no_id(VALID_NHS_NUMBER) bad_patient_name_imms = deepcopy(valid_imms) del bad_patient_name_imms["contained"][1]["name"][0]["given"] - bad_patient_name_msg = ( - "contained[?(@.resourceType=='Patient')].name[0].given is a mandatory field" - ) + bad_patient_name_msg = "contained[?(@.resourceType=='Patient')].name[0].given is a mandatory field" fhir_service = FhirService(self.imms_repo) @@ -544,9 +568,7 @@ def test_unauthorised_error_raised_when_user_lacks_permissions(self): """it should raise error when user lacks permissions""" self.mock_redis_client.hget.return_value = "FLU" self.authoriser.authorise.return_value = False - self.imms_repo.create_immunization.return_value = ( - create_covid_19_immunization_dict_no_id() - ) + self.imms_repo.create_immunization.return_value = create_covid_19_immunization_dict_no_id() nhs_number = VALID_NHS_NUMBER req_imms = create_covid_19_immunization_dict_no_id(nhs_number) @@ -575,7 +597,8 @@ def test_update_immunization(self): """it should update Immunization and validate NHS number""" imms_id = "an-id" self.imms_repo.update_immunization.return_value = ( - create_covid_19_immunization_dict(imms_id), 2 + create_covid_19_immunization_dict(imms_id), + 2, ) self.mock_redis_client.hget.return_value = "COVID19" self.authoriser.authorise.return_value = True @@ -595,7 +618,10 @@ def test_update_immunization(self): def test_id_not_present(self): """it should populate id in the message if it is not present""" req_imms_id = "an-id" - self.imms_repo.update_immunization.return_value = create_covid_19_immunization_dict(req_imms_id), 2 + self.imms_repo.update_immunization.return_value = ( + create_covid_19_immunization_dict(req_imms_id), + 2, + ) self.authoriser.authorise.return_value = True req_imms = create_covid_19_immunization_dict("we-will-remove-this-id") @@ -645,9 +671,7 @@ def test_reinstate_immunization_returns_updated_version(self): self.mock_redis_client.hget.return_value = "COVID19" self.imms_repo.reinstate_immunization.return_value = (req_imms, 5) - outcome, resource, version = self.fhir_service.reinstate_immunization( - imms_id, req_imms, 1, "COVID19", "Test" - ) + outcome, resource, version = self.fhir_service.reinstate_immunization(imms_id, req_imms, 1, "COVID19", "Test") self.assertEqual(outcome, UpdateOutcome.UPDATE) self.assertEqual(version, 5) @@ -686,9 +710,7 @@ def test_reinstate_immunization_with_diagnostics(self): req_imms = create_covid_19_immunization_dict(imms_id) self.fhir_service._validate_patient = MagicMock(return_value={"diagnostics": "invalid patient"}) - outcome, resource, version = self.fhir_service.reinstate_immunization( - imms_id, req_imms, 1, "COVID19", "Test" - ) + outcome, resource, version = self.fhir_service.reinstate_immunization(imms_id, req_imms, 1, "COVID19", "Test") self.assertIsNone(outcome) self.assertEqual(resource, {"diagnostics": "invalid patient"}) @@ -713,6 +735,7 @@ def test_update_reinstated_immunization_with_diagnostics(self): class TestDeleteImmunization(TestFhirServiceBase): """Tests for FhirService.delete_immunization""" + TEST_IMMUNISATION_ID = "an-id" def setUp(self): @@ -720,9 +743,7 @@ def setUp(self): self.authoriser = create_autospec(Authoriser) self.imms_repo = create_autospec(ImmunizationRepository) self.validator = create_autospec(ImmunizationValidator) - self.fhir_service = FhirService( - self.imms_repo, self.authoriser, self.validator - ) + self.fhir_service = FhirService(self.imms_repo, self.authoriser, self.validator) def test_delete_immunization(self): """it should delete Immunization record""" @@ -754,7 +775,9 @@ def test_delete_immunization_throws_not_found_exception_if_does_not_exist(self): self.imms_repo.get_immunization_and_version_by_id.assert_called_once_with(self.TEST_IMMUNISATION_ID) self.imms_repo.delete_immunization.assert_not_called() - def test_delete_immunization_throws_authorisation_exception_if_does_not_have_required_permissions(self): + def test_delete_immunization_throws_authorisation_exception_if_does_not_have_required_permissions( + self, + ): imms = json.loads(create_covid_19_immunization(self.TEST_IMMUNISATION_ID).json()) self.mock_redis_client.hget.return_value = "FLU" self.authoriser.authorise.return_value = False @@ -772,6 +795,7 @@ def test_delete_immunization_throws_authorisation_exception_if_does_not_have_req class TestSearchImmunizations(unittest.TestCase): """Tests for FhirService.search_immunizations""" + MOCK_SUPPLIER_SYSTEM_NAME = "Test" def setUp(self): @@ -795,8 +819,7 @@ def test_vaccine_type_search(self): self.imms_repo.find_immunizations.return_value = [] # When - _ = self.fhir_service.search_immunizations(nhs_number, [vaccine_type], - params, self.MOCK_SUPPLIER_SYSTEM_NAME) + _ = self.fhir_service.search_immunizations(nhs_number, [vaccine_type], params, self.MOCK_SUPPLIER_SYSTEM_NAME) # Then self.authoriser.filter_permitted_vacc_types.assert_called_once_with( @@ -814,8 +837,9 @@ def test_make_fhir_bundle_from_search_result(self): params = f"{self.nhs_search_param}={nhs_number}&{self.vaccine_type_search_param}={vaccine_types}" self.authoriser.filter_permitted_vacc_types.return_value = set(vaccine_types) # When - result, contains_unauthorised_vaccs = self.fhir_service.search_immunizations(nhs_number, vaccine_types, params, - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, contains_unauthorised_vaccs = self.fhir_service.search_immunizations( + nhs_number, vaccine_types, params, self.MOCK_SUPPLIER_SYSTEM_NAME + ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] # Then self.assertIsInstance(result, FhirBundle) @@ -834,7 +858,10 @@ def test_make_fhir_bundle_from_search_result(self): def test_date_from_is_used_to_filter(self): """It should return only Immunizations after date_from""" # Arrange - imms = [("imms-1", "2021-02-07T13:28:17.271+00:00"),("imms-2", "2021-02-08T13:28:17.271+00:00"),] + imms = [ + ("imms-1", "2021-02-07T13:28:17.271+00:00"), + ("imms-2", "2021-02-08T13:28:17.271+00:00"), + ] imms_list = [ create_covid_19_immunization_dict(imms_id, occurrence_date_time=occcurrence_date_time) for (imms_id, occcurrence_date_time) in imms @@ -849,8 +876,11 @@ def test_date_from_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_from=datetime.date(2021, 2, 6) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_from=datetime.date(2021, 2, 6), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -864,8 +894,11 @@ def test_date_from_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_from=datetime.date(2021, 2, 7) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_from=datetime.date(2021, 2, 7), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -879,7 +912,11 @@ def test_date_from_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, date_from=datetime.date(2021, 2, 8) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_from=datetime.date(2021, 2, 8), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -892,7 +929,11 @@ def test_date_from_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME,date_from=datetime.date(2021, 2, 9) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_from=datetime.date(2021, 2, 9), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -912,8 +953,7 @@ def test_date_from_is_optional(self): self.imms_repo.find_immunizations.return_value = deepcopy(imms_list) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] # Then @@ -925,8 +965,11 @@ def test_date_from_is_optional(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_from=datetime.date(2021, 3, 6) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_from=datetime.date(2021, 3, 6), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -937,7 +980,10 @@ def test_date_from_is_optional(self): def test_date_to_is_used_to_filter(self): """It should return only Immunizations before date_to""" # Arrange - imms = [("imms-1", "2021-02-07T13:28:17.271+00:00"),("imms-2", "2021-02-08T13:28:17.271+00:00")] + imms = [ + ("imms-1", "2021-02-07T13:28:17.271+00:00"), + ("imms-2", "2021-02-08T13:28:17.271+00:00"), + ] imms_list = [ create_covid_19_immunization_dict(imms_id, occurrence_date_time=occcurrence_date_time) for (imms_id, occcurrence_date_time) in imms @@ -952,8 +998,11 @@ def test_date_to_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_to=datetime.date(2021, 2, 9) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_to=datetime.date(2021, 2, 9), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -967,8 +1016,11 @@ def test_date_to_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_to=datetime.date(2021, 2, 8) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_to=datetime.date(2021, 2, 8), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -982,8 +1034,11 @@ def test_date_to_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_to=datetime.date(2021, 2, 7) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_to=datetime.date(2021, 2, 7), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -996,8 +1051,11 @@ def test_date_to_is_used_to_filter(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_to=datetime.date(2021, 2, 6) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_to=datetime.date(2021, 2, 6), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -1017,8 +1075,7 @@ def test_date_to_is_optional(self): self.imms_repo.find_immunizations.return_value = deepcopy(imms_list) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] # Then @@ -1030,8 +1087,11 @@ def test_date_to_is_optional(self): # When result, _ = self.fhir_service.search_immunizations( - nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME, - date_to=datetime.date(2021, 3, 8) + nhs_number, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, + date_to=datetime.date(2021, 3, 8), ) searched_imms = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] @@ -1047,7 +1107,11 @@ def test_immunization_resources_are_filtered_for_search(self): # Arrange imms_ids = ["imms-1", "imms-2"] imms_list = [ - create_covid_19_immunization_dict(imms_id,NHS_NUMBER_USED_IN_SAMPLE_DATA,occurrence_date_time="2021-02-07T13:28:17+00:00") + create_covid_19_immunization_dict( + imms_id, + NHS_NUMBER_USED_IN_SAMPLE_DATA, + occurrence_date_time="2021-02-07T13:28:17+00:00", + ) for imms_id in imms_ids ] @@ -1057,7 +1121,10 @@ def test_immunization_resources_are_filtered_for_search(self): # When result, _ = self.fhir_service.search_immunizations( - NHS_NUMBER_USED_IN_SAMPLE_DATA, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME + NHS_NUMBER_USED_IN_SAMPLE_DATA, + vaccine_types, + "", + self.MOCK_SUPPLIER_SYSTEM_NAME, ) searched_imms = [ json.loads(entry.json(), parse_float=Decimal) @@ -1065,9 +1132,7 @@ def test_immunization_resources_are_filtered_for_search(self): if entry.resource.resource_type == "Immunization" ] searched_patient = [ - json.loads(entry.json()) - for entry in result.entry - if entry.resource.resource_type == "Patient" + json.loads(entry.json()) for entry in result.entry if entry.resource.resource_type == "Patient" ][0] # Then @@ -1096,8 +1161,7 @@ def test_matches_contain_fullUrl(self): self.authoriser.filter_permitted_vacc_types.return_value = set(vaccine_types) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) entries = [entry for entry in result.entry if entry.resource.resource_type == "Immunization"] # Then @@ -1119,11 +1183,13 @@ def test_patient_contains_fullUrl(self): self.authoriser.filter_permitted_vacc_types.return_value = set(vaccine_types) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) # Then - patient_entry = next((entry for entry in result.entry if entry.resource.resource_type == "Patient"), None) + patient_entry = next( + (entry for entry in result.entry if entry.resource.resource_type == "Patient"), + None, + ) patient_full_url = patient_entry.fullUrl self.assertTrue(patient_full_url.startswith("urn:uuid:")) @@ -1136,15 +1202,13 @@ def test_patient_included(self): imms_ids = ["imms-1", "imms-2"] imms_list = [create_covid_19_immunization_dict(imms_id) for imms_id in imms_ids] - patient = next(contained for contained in imms_list[0]["contained"] if contained["resourceType"] == "Patient") self.imms_repo.find_immunizations.return_value = imms_list nhs_number = VALID_NHS_NUMBER vaccine_types = ["COVID19"] self.authoriser.filter_permitted_vacc_types.return_value = set(vaccine_types) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) # Then patient_entry = next((entry for entry in result.entry if entry.resource.resource_type == "Patient")) @@ -1155,15 +1219,13 @@ def test_patient_is_stripped(self): imms_ids = ["imms-1", "imms-2"] imms_list = [create_covid_19_immunization_dict(imms_id) for imms_id in imms_ids] - patient = next(contained for contained in imms_list[0]["contained"] if contained["resourceType"] == "Patient") self.imms_repo.find_immunizations.return_value = imms_list nhs_number = VALID_NHS_NUMBER vaccine_types = ["COVID19"] self.authoriser.filter_permitted_vacc_types.return_value = set(vaccine_types) # When - result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", - self.MOCK_SUPPLIER_SYSTEM_NAME) + result, _ = self.fhir_service.search_immunizations(nhs_number, vaccine_types, "", self.MOCK_SUPPLIER_SYSTEM_NAME) # Then patient_entry = next((entry for entry in result.entry if entry.resource.resource_type == "Patient")) @@ -1189,8 +1251,9 @@ def test_search_raises_unauthorised_error_if_no_permissions(self): # When with self.assertRaises(UnauthorizedVaxError): - self.fhir_service.search_immunizations(VALID_NHS_NUMBER, [vaccine_type], - params, self.MOCK_SUPPLIER_SYSTEM_NAME) + self.fhir_service.search_immunizations( + VALID_NHS_NUMBER, [vaccine_type], params, self.MOCK_SUPPLIER_SYSTEM_NAME + ) # Then self.authoriser.filter_permitted_vacc_types.assert_called_once_with( @@ -1198,7 +1261,9 @@ def test_search_raises_unauthorised_error_if_no_permissions(self): ) self.imms_repo.find_immunizations.assert_not_called() - def test_search_returns_successfully_but_flags_if_supplier_requests_vacc_types_without_perms(self): + def test_search_returns_successfully_but_flags_if_supplier_requests_vacc_types_without_perms( + self, + ): """It should return a boolean indicating if the supplier has requested one or more vaccination types which they do not have permission to search. This is a more permissive model and ensures that results are still returned for the vacc types that they can handle""" diff --git a/backend/tests/test_api_errors.py b/backend/tests/test_api_errors.py index 949f057df..e10618998 100644 --- a/backend/tests/test_api_errors.py +++ b/backend/tests/test_api_errors.py @@ -1,8 +1,10 @@ import unittest from models.errors import Severity, Code, create_operation_outcome + "test" + class TestApiErrors(unittest.TestCase): def test_error_to_uk_core2(self): code = Code.not_found diff --git a/backend/tests/test_cache.py b/backend/tests/test_cache.py index c94fd1b22..e4d8754b2 100644 --- a/backend/tests/test_cache.py +++ b/backend/tests/test_cache.py @@ -5,6 +5,8 @@ from cache import Cache "test" + + class TestCache(unittest.TestCase): def setUp(self): self.cache = Cache(tempfile.gettempdir()) diff --git a/backend/tests/test_create_imms.py b/backend/tests/test_create_imms.py index 3bf2bc585..d34a433ba 100644 --- a/backend/tests/test_create_imms.py +++ b/backend/tests/test_create_imms.py @@ -2,10 +2,10 @@ import unittest from unittest.mock import create_autospec, patch -from create_imms_handler import create_immunization +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.fhir_controller import FhirController +from create_imms_handler import create_immunization from models.errors import Severity, Code, create_operation_outcome -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE class TestCreateImmunizationById(unittest.TestCase): diff --git a/backend/tests/test_delete_imms.py b/backend/tests/test_delete_imms.py index 513549f98..34b0c434a 100644 --- a/backend/tests/test_delete_imms.py +++ b/backend/tests/test_delete_imms.py @@ -2,10 +2,10 @@ import unittest from unittest.mock import create_autospec, patch -from delete_imms_handler import delete_immunization +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.fhir_controller import FhirController +from delete_imms_handler import delete_immunization from models.errors import Severity, Code, create_operation_outcome -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE class TestDeleteImmunizationById(unittest.TestCase): diff --git a/backend/tests/test_filter.py b/backend/tests/test_filter.py index e18a6a4cf..d2148ddb1 100644 --- a/backend/tests/test_filter.py +++ b/backend/tests/test_filter.py @@ -1,10 +1,9 @@ """Tests for Filter class""" +import unittest from copy import deepcopy from uuid import uuid4 -import unittest - from constants import Urls from filter import ( Filter, @@ -76,10 +75,16 @@ def test_create_reference_to_patient_resource(self): expected_output = { "reference": patient_uuid, "type": "Patient", - "identifier": {"system": "https://fhir.nhs.uk/Id/nhs-number", "value": "9000000009"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + }, } - self.assertEqual(create_reference_to_patient_resource(patient_uuid, input_data), expected_output) + self.assertEqual( + create_reference_to_patient_resource(patient_uuid, input_data), + expected_output, + ) def test_add_use_to_identifier(self): """Test that a use of "offical" is added to identifier[0] is no use already given""" @@ -117,7 +122,10 @@ def test_replace_organization_values(self): # TEST CASE: Input data has organization identifier value and system input_imms_data = deepcopy(input_imms) expected_output_data = deepcopy(expected_output) - expected_organization_identifier = {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "N2N9I"} + expected_organization_identifier = { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "N2N9I", + } expected_output_data["performer"][1]["actor"]["identifier"] = expected_organization_identifier self.assertEqual(replace_organization_values(deepcopy(input_imms)), expected_output_data) @@ -125,7 +133,10 @@ def test_replace_organization_values(self): input_imms_data = deepcopy(input_imms) del input_imms_data["performer"][1]["actor"]["identifier"]["system"] expected_output_data = deepcopy(expected_output) - expected_organization_identifier = {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "N2N9I"} + expected_organization_identifier = { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "N2N9I", + } expected_output_data["performer"][1]["actor"]["identifier"] = expected_organization_identifier self.assertEqual(replace_organization_values(input_imms_data), expected_output_data) diff --git a/backend/tests/test_forwarding_batch_lambda.py b/backend/tests/test_forwarding_batch_lambda.py index b83e28f9e..3b33b56af 100644 --- a/backend/tests/test_forwarding_batch_lambda.py +++ b/backend/tests/test_forwarding_batch_lambda.py @@ -1,10 +1,15 @@ -import unittest +import base64 +import copy +import json import os +import unittest from typing import Optional from unittest import TestCase from unittest.mock import patch, MagicMock, ANY + from boto3 import resource as boto3_resource from moto import mock_aws + from models.errors import ( MessageNotSuccessfulError, RecordProcessorError, @@ -13,14 +18,13 @@ ResourceNotFoundError, ResourceFoundError, ) -import base64 -import copy -import json - from testing_utils.test_utils_for_batch import ForwarderValues, MockFhirImmsResources with patch.dict("os.environ", ForwarderValues.MOCK_ENVIRONMENT_DICT): - from forwarding_batch_lambda import forward_lambda_handler, create_diagnostics_dictionary + from forwarding_batch_lambda import ( + forward_lambda_handler, + create_diagnostics_dictionary, + ) @mock_aws @@ -46,7 +50,10 @@ def setUp(self): "IndexName": "IdentifierGSI", "KeySchema": [{"AttributeName": "IdentifierPK", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, { "IndexName": "PatientGSI", @@ -54,7 +61,10 @@ def setUp(self): {"AttributeName": "PatientPK", "KeyType": "HASH"}, ], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, ], ) @@ -75,8 +85,11 @@ def generate_fhir_json(include_fhir_json=True, identifier_value=None, operation_ if not include_fhir_json: return None - fhir_json = copy.deepcopy(MockFhirImmsResources.all_fields) if operation_requested != "DELETE" else ( - copy.deepcopy(MockFhirImmsResources.delete_operation_fields)) + fhir_json = ( + copy.deepcopy(MockFhirImmsResources.all_fields) + if operation_requested != "DELETE" + else (copy.deepcopy(MockFhirImmsResources.delete_operation_fields)) + ) if fhir_json.get("identifier") and identifier_value is not None: fhir_json["identifier"][0]["value"] = identifier_value @@ -85,7 +98,10 @@ def generate_fhir_json(include_fhir_json=True, identifier_value=None, operation_ @staticmethod def generate_details_from_processing( - include_fhir_json=True, operation_requested="CREATE", local_id="local-1", identifier_value=None + include_fhir_json=True, + operation_requested="CREATE", + local_id="local-1", + identifier_value=None, ): """Helper to generate details_from_processing for row data.""" details = { @@ -94,9 +110,7 @@ def generate_details_from_processing( } if include_fhir_json: details["fhir_json"] = TestForwardLambdaHandler.generate_fhir_json( - include_fhir_json, - identifier_value, - operation_requested + include_fhir_json, identifier_value, operation_requested ) return details @@ -109,7 +123,7 @@ def generate_input( include_fhir_json: bool = True, operation_requested: str = "create", diagnostics: dict = None, - supplier: str = "test_supplier" + supplier: str = "test_supplier", ): """Generates input rows for test_cases.""" details_from_processing = TestForwardLambdaHandler.generate_details_from_processing( @@ -207,7 +221,10 @@ def test_forward_lambda_handler_single_operations(self, mock_send_message): identifier_value="single_test_create", ), "expected_keys": ForwarderValues.EXPECTED_KEYS, - "expected_values": {"row_id": "row-1", **ForwarderValues.EXPECTED_VALUES}, + "expected_values": { + "row_id": "row-1", + **ForwarderValues.EXPECTED_VALUES, + }, "expected_dynamo_item": { "IdentifierPK": "https://www.ravs.england.nhs.uk/#single_test_create", "Operation": "CREATE", @@ -217,7 +234,11 @@ def test_forward_lambda_handler_single_operations(self, mock_send_message): "name": "Single Update Success", "input": self.generate_input(row_id=1, operation_requested="UPDATE", include_fhir_json=True), "expected_keys": ForwarderValues.EXPECTED_KEYS, - "expected_values": {"row_id": "row-1", "imms_id": pk_test, **ForwarderValues.EXPECTED_VALUES}, + "expected_values": { + "row_id": "row-1", + "imms_id": pk_test, + **ForwarderValues.EXPECTED_VALUES, + }, "expected_dynamo_item": table_item, }, { @@ -280,10 +301,16 @@ def test_forward_lambda_handler_multiple_scenarios(self, mock_send_message): { "name": "Row 1: Create Success", "input": self.generate_input( - row_id=1, operation_requested="CREATE", include_fhir_json=True, identifier_value="RSV_CREATE" + row_id=1, + operation_requested="CREATE", + include_fhir_json=True, + identifier_value="RSV_CREATE", ), "expected_keys": ForwarderValues.EXPECTED_KEYS, - "expected_values": {"row_id": "row-1", **ForwarderValues.EXPECTED_VALUES}, + "expected_values": { + "row_id": "row-1", + **ForwarderValues.EXPECTED_VALUES, + }, }, { "name": "Row 2: Duplication Error: Create failure ", @@ -300,19 +327,26 @@ def test_forward_lambda_handler_multiple_scenarios(self, mock_send_message): "name": "Row 3: Update success", "input": self.generate_input(row_id=3, operation_requested="UPDATE", include_fhir_json=True), "expected_keys": ForwarderValues.EXPECTED_KEYS, - "expected_values": {"row_id": "row-3", "imms_id": "Immunization#4d2ac1eb-080f-4e54-9598-f2d53334681c"}, + "expected_values": { + "row_id": "row-3", + "imms_id": "Immunization#4d2ac1eb-080f-4e54-9598-f2d53334681c", + }, }, { "name": "Row 4: Update failure", "input": self.generate_input( - row_id=4, operation_requested="UPDATE", include_fhir_json=True, identifier_value="RSV_UPDATE" + row_id=4, + operation_requested="UPDATE", + include_fhir_json=True, + identifier_value="RSV_UPDATE", ), "expected_keys": ForwarderValues.EXPECTED_KEYS_DIAGNOSTICS, "expected_values": { "row_id": "row-4", "diagnostics": create_diagnostics_dictionary( ResourceNotFoundError( - resource_type="Immunization", resource_id="https://www.ravs.england.nhs.uk/#RSV_UPDATE" + resource_type="Immunization", + resource_id="https://www.ravs.england.nhs.uk/#RSV_UPDATE", ) ), }, @@ -321,7 +355,10 @@ def test_forward_lambda_handler_multiple_scenarios(self, mock_send_message): "name": "Row 5: Delete Success", "input": self.generate_input(row_id=5, operation_requested="DELETE", include_fhir_json=True), "expected_keys": ForwarderValues.EXPECTED_KEYS, - "expected_values": {"row_id": "row-5", "imms_id": "Immunization#4d2ac1eb-080f-4e54-9598-f2d53334681c"}, + "expected_values": { + "row_id": "row-5", + "imms_id": "Immunization#4d2ac1eb-080f-4e54-9598-f2d53334681c", + }, }, { "name": "Row 6: Delete Failure", @@ -424,7 +461,7 @@ def test_forward_lambda_handler_groups_and_sends_events_by_filename(self, mock_s identifier_value="supplier_1_system/54321", operation_requested="CREATE", file_key="supplier_1_rsv_test_file", - supplier="supplier_1" + supplier="supplier_1", ) }, { @@ -433,9 +470,9 @@ def test_forward_lambda_handler_groups_and_sends_events_by_filename(self, mock_s identifier_value="supplier_2_system/12345", operation_requested="CREATE", file_key="supplier_2_rsv_test_file", - supplier="supplier_2" + supplier="supplier_2", ) - } + }, ] mock_kinesis_event = self.generate_event(mock_records) self.mock_redis_client.hget.return_value = "RSV" @@ -448,33 +485,45 @@ def test_forward_lambda_handler_groups_and_sends_events_by_filename(self, mock_s # Separate calls are made for each of the respective groups self.assertEqual(len(sqs_calls), 2) - self.assertEqual(first_call_kwargs["MessageGroupId"], "supplier_1_rsv_test_file_2025-01-24T12:00:00Z") - self.assertEqual(second_call_kwargs["MessageGroupId"], "supplier_2_rsv_test_file_2025-01-24T12:00:00Z") - - self.assertDictEqual(json.loads(first_call_kwargs["MessageBody"])[0], { - "created_at_formatted_string": "2025-01-24T12:00:00Z", - "file_key": "supplier_1_rsv_test_file", - "operation_start_time": ANY, - "operation_end_time": ANY, - "imms_id": ANY, - "local_id": "local-1", - "operation_requested": "CREATE", - "row_id": "row-1", - "supplier": "supplier_1", - "vaccine_type": "RSV" - }) - self.assertDictEqual(json.loads(second_call_kwargs["MessageBody"])[0], { - "created_at_formatted_string": "2025-01-24T12:00:00Z", - "file_key": "supplier_2_rsv_test_file", - "operation_start_time": ANY, - "operation_end_time": ANY, - "imms_id": ANY, - "local_id": "local-2", - "operation_requested": "CREATE", - "row_id": "row-2", - "supplier": "supplier_2", - "vaccine_type": "RSV" - }) + self.assertEqual( + first_call_kwargs["MessageGroupId"], + "supplier_1_rsv_test_file_2025-01-24T12:00:00Z", + ) + self.assertEqual( + second_call_kwargs["MessageGroupId"], + "supplier_2_rsv_test_file_2025-01-24T12:00:00Z", + ) + + self.assertDictEqual( + json.loads(first_call_kwargs["MessageBody"])[0], + { + "created_at_formatted_string": "2025-01-24T12:00:00Z", + "file_key": "supplier_1_rsv_test_file", + "operation_start_time": ANY, + "operation_end_time": ANY, + "imms_id": ANY, + "local_id": "local-1", + "operation_requested": "CREATE", + "row_id": "row-1", + "supplier": "supplier_1", + "vaccine_type": "RSV", + }, + ) + self.assertDictEqual( + json.loads(second_call_kwargs["MessageBody"])[0], + { + "created_at_formatted_string": "2025-01-24T12:00:00Z", + "file_key": "supplier_2_rsv_test_file", + "operation_start_time": ANY, + "operation_end_time": ANY, + "imms_id": ANY, + "local_id": "local-2", + "operation_requested": "CREATE", + "row_id": "row-2", + "supplier": "supplier_2", + "vaccine_type": "RSV", + }, + ) @patch("forwarding_batch_lambda.sqs_client.send_message") def test_forward_lambda_handler_update_scenarios(self, mock_send_message): @@ -499,7 +548,10 @@ def test_forward_lambda_handler_update_scenarios(self, mock_send_message): { "name": "Row 1a: Update existing record", "input": self.generate_input( - row_id=1, operation_requested="UPDATE", include_fhir_json=True, identifier_value="UPDATE_TEST" + row_id=1, + operation_requested="UPDATE", + include_fhir_json=True, + identifier_value="UPDATE_TEST", ), "expected_keys": ForwarderValues.EXPECTED_KEYS, "expected_values": {"row_id": "row-1", "imms_id": pk_test_update}, @@ -515,7 +567,10 @@ def test_forward_lambda_handler_update_scenarios(self, mock_send_message): { "name": "Row 2a: Delete the updated record", "input": self.generate_input( - row_id=2, operation_requested="DELETE", include_fhir_json=True, identifier_value="UPDATE_TEST" + row_id=2, + operation_requested="DELETE", + include_fhir_json=True, + identifier_value="UPDATE_TEST", ), "expected_keys": ForwarderValues.EXPECTED_KEYS, "expected_values": {"row_id": "row-2", "imms_id": pk_test_update}, @@ -531,7 +586,10 @@ def test_forward_lambda_handler_update_scenarios(self, mock_send_message): { "name": "Row 3a: Delete Error to Confirm record does not exist anymore", "input": self.generate_input( - row_id=3, operation_requested="DELETE", include_fhir_json=True, identifier_value="UPDATE_TEST" + row_id=3, + operation_requested="DELETE", + include_fhir_json=True, + identifier_value="UPDATE_TEST", ), "expected_keys": ForwarderValues.EXPECTED_KEYS_DIAGNOSTICS, "expected_values": { @@ -548,7 +606,10 @@ def test_forward_lambda_handler_update_scenarios(self, mock_send_message): { "name": "Row 4a: Reinstated record using Update operation", "input": self.generate_input( - row_id=4, operation_requested="UPDATE", include_fhir_json=True, identifier_value="UPDATE_TEST" + row_id=4, + operation_requested="UPDATE", + include_fhir_json=True, + identifier_value="UPDATE_TEST", ), "expected_keys": ForwarderValues.EXPECTED_KEYS, "expected_values": {"row_id": "row-4", "imms_id": pk_test_update}, @@ -651,7 +712,11 @@ def test_create_diagnostics_dictionary(self): @patch("forwarding_batch_lambda.create_table") @patch("forwarding_batch_lambda.make_batch_controller") def test_forward_request_to_dynamo( - self, mock_make_controller, mock_create_table, mock_forward_request_to_dynamo, mock_send_message + self, + mock_make_controller, + mock_create_table, + mock_forward_request_to_dynamo, + mock_send_message, ): """Test forward lambda handler to assert dynamo db is called, and diagnostics handling. @@ -661,7 +726,7 @@ def test_forward_request_to_dynamo( expected_values (dict): expected output dictionary values """ mock_create_table.return_value = {} - mock_make_controller.return_value = mock_controller = MagicMock() + mock_make_controller.return_value = MagicMock() mock_forward_request_to_dynamo.side_effect = [ "IMMS123", ] diff --git a/backend/tests/test_get_imms.py b/backend/tests/test_get_imms.py index e91ba3ac0..887a160fc 100644 --- a/backend/tests/test_get_imms.py +++ b/backend/tests/test_get_imms.py @@ -5,7 +5,6 @@ from get_imms_handler import get_immunization_by_id - class TestGetImmunisationById(unittest.TestCase): def setUp(self): self.controller = create_autospec(FhirController) diff --git a/backend/tests/test_immunization_post_validator.py b/backend/tests/test_immunization_post_validator.py index 2bd98d47e..f04e1226e 100644 --- a/backend/tests/test_immunization_post_validator.py +++ b/backend/tests/test_immunization_post_validator.py @@ -1,12 +1,11 @@ """Test immunization pre validation rules on the model""" import unittest -from unittest.mock import patch -import json from copy import deepcopy -from pydantic import ValidationError -from jsonpath_ng.ext import parse +from unittest.mock import patch +from jsonpath_ng.ext import parse +from pydantic import ValidationError from models.fhir_immunization import ImmunizationValidator from testing_utils.generic_utils import ( @@ -14,9 +13,10 @@ test_invalid_values_rejected as _test_invalid_values_rejected, load_json_data, ) +from testing_utils.generic_utils import update_contained_resource_field from testing_utils.mandation_test_utils import MandationTests from testing_utils.values_for_tests import NameInstances -from testing_utils.generic_utils import update_contained_resource_field + class TestImmunizationModelPostValidationRules(unittest.TestCase): """Test immunization post validation rules on the FHIR model""" @@ -49,7 +49,7 @@ def test_collected_errors(self): """Test that when passed multiple validation errors, it returns a list of all expected errors""" covid_19_json_data = deepcopy(self.completed_json_data["COVID19"]) - self.mock_redis_client.hget.return_value = 'COVID19' + self.mock_redis_client.hget.return_value = "COVID19" for patient in covid_19_json_data["contained"]: if patient["resourceType"] == "Patient": for name in patient["name"]: @@ -97,10 +97,14 @@ def test_post_validate_and_set_vaccine_type(self): "protocolApplied[0].targetDisease[0].coding[?(@.system=='http://snomed.info/sct')].code" ) - self.mock_redis_client.hget.side_effect = [ "COVID19", "FLU", + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", "HPV", "MMR", - "RSV", None] + "RSV", + None, + ] # Test that a valid combination of disease codes is accepted for vaccine_type in [ "COVID19", @@ -125,9 +129,33 @@ def test_post_validate_and_set_vaccine_type(self): self.mock_redis_client.hget.return_value = None # Test that an invalid combination of disease codes is rejected invalid_target_disease = [ - {"coding": [{"system": "http://snomed.info/sct", "code": "14189004", "display": "Measles"}]}, - {"coding": [{"system": "http://snomed.info/sct", "code": "INVALID", "display": "Mumps"}]}, - {"coding": [{"system": "http://snomed.info/sct", "code": "36653000", "display": "Rubella"}]}, + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "14189004", + "display": "Measles", + } + ] + }, + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "INVALID", + "display": "Mumps", + } + ] + }, + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "36653000", + "display": "Rubella", + } + ] + }, ] _test_invalid_values_rejected( @@ -142,7 +170,9 @@ def test_post_validate_and_set_vaccine_type(self): # Test that json data which doesn't contain a targetDisease code is rejected expected_error_message = f"{first_target_disease_code_field_location} is a mandatory field" - MandationTests.test_missing_mandatory_field_rejected(self, first_target_disease_code_field_location, None, expected_error_message) + MandationTests.test_missing_mandatory_field_rejected( + self, first_target_disease_code_field_location, None, expected_error_message + ) def test_post_vaccination_procedure_code(self): """Test that the JSON data is rejected if it does not contain vaccination_procedure_code""" @@ -388,7 +418,7 @@ def test_post_dose_number_positive_int(self): # dose_number_positive_int exists, dose_number_string does not exist valid_json_data = deepcopy(self.completed_json_data[vaccine_type]) self.mock_redis_client.hget.side_effect = None - self.mock_redis_client.hget.return_value = 'COVID19' + self.mock_redis_client.hget.return_value = "COVID19" MandationTests.test_present_field_accepted(self, valid_json_data) # dose_number_positive_int does not exist, dose_number_string exists @@ -422,7 +452,13 @@ def test_post_manufacturer_display(self): def test_post_lot_number(self): """Test that present or absent lot_number is accepted or rejected as appropriate dependent on other fields""" - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "lotNumber" for vaccine_type in self.all_vaccine_types: MandationTests.test_missing_field_accepted(self, field_location, self.completed_json_data[vaccine_type]) @@ -432,7 +468,13 @@ def test_post_expiration_date(self): Test that present or absent expiration_date is accepted or rejected as appropriate dependent on other fields """ - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "expirationDate" for vaccine_type in self.all_vaccine_types: MandationTests.test_missing_field_accepted(self, field_location, self.completed_json_data[vaccine_type]) @@ -470,7 +512,13 @@ def test_post_dose_quantity_value(self): """ Test that present or absent dose_quantity_value is accepted or rejected as appropriate dependent on other fields """ - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "doseQuantity.value" for vaccine_type in self.all_vaccine_types: MandationTests.test_missing_field_accepted(self, field_location, self.completed_json_data[vaccine_type]) @@ -479,7 +527,13 @@ def test_post_dose_quantity_code(self): """ Test that present or absent dose_quantity_code is accepted or rejected as appropriate dependent on other fields """ - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "doseQuantity.code" for vaccine_type in self.all_vaccine_types: MandationTests.test_missing_field_accepted(self, field_location, self.completed_json_data[vaccine_type]) @@ -487,7 +541,7 @@ def test_post_dose_quantity_code(self): def test_post_dose_quantity_unit(self): """Test that the JSON data is accepted when dose_quantity_unit is absent""" self.mock_redis_client.hget.side_effect = None - self.mock_redis_client.hget.return_value = 'COVID19' + self.mock_redis_client.hget.return_value = "COVID19" MandationTests.test_missing_field_accepted(self, "doseQuantity.unit") # NOTE: THIS TEST IS COMMENTED OUT AS IT IS TESTING A REQUIRED ELEMENT (VALIDATION SHOULD ALWAYS PASS), @@ -516,21 +570,32 @@ def test_post_pre_validate_extension_url(self): self.mock_redis_client.hget.side_effect = None self.mock_redis_client.hget.return_value = "COVID19" invalid_json_data = deepcopy(self.completed_json_data["COVID19"]) - invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"][0]["system"]='https://xyz/Extension-UKCore-VaccinationProcedure' + invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"][0][ + "system" + ] = "https://xyz/Extension-UKCore-VaccinationProcedure" with self.assertRaises(Exception) as error: self.validator.validate(invalid_json_data) full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code is a mandatory field", actual_error_messages) + self.assertIn( + "extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code is a mandatory field", + actual_error_messages, + ) def test_post_location_identifier_value(self): """ Test that the JSON data is rejected if it does and does not contain location_identifier_value as appropriate """ - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "location.identifier.value" # Test cases for COVID-19, FLU, HPV and MMR where it is mandatory for vaccine_type in ( @@ -547,7 +612,13 @@ def test_post_location_identifier_system(self): """ Test that the JSON data is rejected if it does and does not contain location_identifier_system as appropriate """ - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'RSV'] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "RSV", + ] field_location = "location.identifier.system" # Test cases for COVID-19, FLU, HPV and MMR where it is mandatory for vaccine_type in ( @@ -567,9 +638,25 @@ def test_post_no_snomed_code(self): covid_19_json_data = deepcopy(self.completed_json_data["COVID19"]) invalid_target_disease_value = [ - {"coding": [{"system": "http://snomed.info/sct", "code": "14189004", "display": "Measles"}]}, + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "14189004", + "display": "Measles", + } + ] + }, {"coding": [{"system": "http://snomed.info/sct", "display": "Mumps"}]}, - {"coding": [{"system": "http://snomed.info/sct", "code": "36653000", "display": "Rubella"}]}, + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "36653000", + "display": "Rubella", + } + ] + }, ] covid_19_json_data["protocolApplied"][0]["targetDisease"] = invalid_target_disease_value diff --git a/backend/tests/test_immunization_pre_validator.py b/backend/tests/test_immunization_pre_validator.py index adfe3ade2..a259106c4 100644 --- a/backend/tests/test_immunization_pre_validator.py +++ b/backend/tests/test_immunization_pre_validator.py @@ -8,27 +8,27 @@ from jsonpath_ng.ext import parse from models.fhir_immunization import ImmunizationValidator +from models.fhir_immunization_pre_validators import PreValidators from models.utils.generic_utils import get_generic_extension_value -from testing_utils.generic_utils import ( - # these have an underscore to avoid pytest collecting them as tests - test_valid_values_accepted as _test_valid_values_accepted, - test_invalid_values_rejected as _test_invalid_values_rejected, - load_json_data, -) from models.utils.generic_utils import ( patient_name_given_field_location, patient_name_family_field_location, practitioner_name_given_field_location, practitioner_name_family_field_location, ) +from testing_utils.generic_utils import ( + # these have an underscore to avoid pytest collecting them as tests + test_valid_values_accepted as _test_valid_values_accepted, + test_invalid_values_rejected as _test_invalid_values_rejected, + load_json_data, +) from testing_utils.pre_validation_test_utils import ValidatorModelTests from testing_utils.values_for_tests import ValidValues, InvalidValues -from models.fhir_immunization_pre_validators import PreValidators + class TestImmunizationModelPreValidationRules(unittest.TestCase): """Test immunization pre validation rules on the FHIR model using the covid sample data""" - def setUp(self): """Set up for each test. This runs before every test""" self.json_data = load_json_data(filename="completed_covid19_immunization_event.json") @@ -248,7 +248,10 @@ def test_pre_validate_contained_contents(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("The contained Patient resource must have an 'id' field", actual_error_messages) + self.assertIn( + "The contained Patient resource must have an 'id' field", + actual_error_messages, + ) # REJECT: Missing practitioner id invalid_json_data = deepcopy(self.json_data) @@ -260,7 +263,10 @@ def test_pre_validate_contained_contents(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("The contained Practitioner resource must have an 'id' field", actual_error_messages) + self.assertIn( + "The contained Practitioner resource must have an 'id' field", + actual_error_messages, + ) # REJECT: Duplicate id invalid_json_data = deepcopy(self.json_data) @@ -272,7 +278,10 @@ def test_pre_validate_contained_contents(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("ids must not be duplicated amongst contained resources", actual_error_messages) + self.assertIn( + "ids must not be duplicated amongst contained resources", + actual_error_messages, + ) def test_pre_validate_patient_reference(self): """Test pre_validate_patient_reference accepts valid values and rejects invalid values""" @@ -326,7 +335,10 @@ def test_pre_validate_practitioner_reference(self): valid_organization = { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, } } valid_practitioner_reference = {"actor": {"reference": "#Pract1"}} @@ -363,7 +375,11 @@ def test_pre_validate_practitioner_reference(self): self, valid_json_data=deepcopy(valid_json_data), field_location=field_location, - invalid_value=[valid_organization, valid_practitioner_reference, invalid_practitioner_reference], + invalid_value=[ + valid_organization, + valid_practitioner_reference, + invalid_practitioner_reference, + ], expected_error_message="performer must not contain any internal references other than" + " to the contained Practitioner resource", ) @@ -390,7 +406,11 @@ def test_pre_validate_practitioner_reference(self): self, valid_json_data=deepcopy(valid_json_data), field_location=field_location, - invalid_value=[valid_organization, valid_practitioner_reference, valid_practitioner_reference], + invalid_value=[ + valid_organization, + valid_practitioner_reference, + valid_practitioner_reference, + ], expected_error_message="contained Practitioner resource id 'Pract1' must only be referenced once" + " from performer", ) @@ -453,7 +473,11 @@ def test_pre_validate_patient_name(self): [ {"family": "Test1", "given": ["TestA"]}, {"use": "official", "family": "Test2", "given": ["TestB"]}, - {"family": "ATest3", "given": ["TestA"], "period": {"start": "2021-02-07T13:28:17+00:00"}}, + { + "family": "ATest3", + "given": ["TestA"], + "period": {"start": "2021-02-07T13:28:17+00:00"}, + }, ] ], valid_list_element=[{"family": "Test", "given": ["TestA"]}], @@ -500,7 +524,13 @@ def test_pre_validate_patient_address(self): ValidatorModelTests.test_list_value( self, field_location="contained[?(@.resourceType=='Patient')].address", - valid_lists_to_test=[[{"postalCode": "AA1 1AA"}, {"postalCode": "75007"}, {"postalCode": "AA11AA"}]], + valid_lists_to_test=[ + [ + {"postalCode": "AA1 1AA"}, + {"postalCode": "75007"}, + {"postalCode": "AA11AA"}, + ] + ], valid_list_element={"family": "Test"}, ) @@ -513,8 +543,8 @@ def test_pre_validate_patient_address_postal_code(self): "address": [ {"city": ""}, {"postalCode": ""}, - {"postalCode": "LS1 MH3"} - ] + {"postalCode": "LS1 MH3"}, + ], } ] } @@ -523,9 +553,7 @@ def test_pre_validate_patient_address_postal_code(self): def test_pre_validate_occurrence_date_time(self): """Test pre_validate_occurrence_date_time accepts valid values and rejects invalid values""" - ValidatorModelTests.test_date_time_value( - self, field_location="occurrenceDateTime", is_occurrence_date_time=True - ) + ValidatorModelTests.test_date_time_value(self, field_location="occurrenceDateTime", is_occurrence_date_time=True) def test_pre_validate_performer(self): """Test pre_validate_performer accepts valid values and rejects invalid values""" @@ -681,7 +709,10 @@ def test_pre_validate_missing_valueCodeableConcept(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept is a mandatory field", actual_error_messages) + self.assertIn( + "extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept is a mandatory field", + actual_error_messages, + ) def test_pre_validate_missing_valueCodeableConcept2(self): # Test case: missing "coding" within "valueCodeableConcept" @@ -693,7 +724,10 @@ def test_pre_validate_missing_valueCodeableConcept2(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding is a mandatory field", actual_error_messages) + self.assertIn( + "extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding is a mandatory field", + actual_error_messages, + ) def test_pre_validate_missing_valueCodeableConcept3(self): # Test case: valid data (should not raise an exception) @@ -704,23 +738,24 @@ def test_pre_validate_missing_valueCodeableConcept3(self): except Exception as error: self.fail(f"Validation unexpectedly raised an exception: {error}") - def test_pre_validate_extension_length(self): """Test test_pre_validate_extension_length accepts valid length of 1 and rejects invalid length for extension""" # Test case: missing "extension" invalid_json_data = deepcopy(self.json_data) - invalid_json_data["extension"].append({ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ + invalid_json_data["extension"].append( { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", + } + ] + }, } - ] - } - }) + ) with self.assertRaises(Exception) as error: self.validator.validate(invalid_json_data) @@ -733,20 +768,30 @@ def test_pre_validate_extension_url1(self): """Test test_pre_validate_extension_url accepts valid values and rejects invalid values for extension[0].url""" # Test case: missing "extension" invalid_json_data = deepcopy(self.json_data) - invalid_json_data["extension"][0]["url"]='https://xyz/Extension-UKCore-VaccinationProcedure' + invalid_json_data["extension"][0]["url"] = "https://xyz/Extension-UKCore-VaccinationProcedure" with self.assertRaises(Exception) as error: self.validator.validate(invalid_json_data) full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("extension[0].url must be one of the following: https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", actual_error_messages) + self.assertIn( + "extension[0].url must be one of the following: https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + actual_error_messages, + ) def test_pre_validate_extension_snomed_code(self): """Test test_pre_validate_extension_url accepts valid values and rejects invalid values for extension[0].url""" # Test case: missing "extension" invalid_json_data = deepcopy(self.json_data) - test_values = ["12345abc", "12345", "1234567890123456789", "12345671", "1324681000000111", "0101291008"] + test_values = [ + "12345abc", + "12345", + "1234567890123456789", + "12345671", + "1324681000000111", + "0101291008", + ] for values in test_values: invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"][0]["code"] = values @@ -755,38 +800,45 @@ def test_pre_validate_extension_snomed_code(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code is not a valid snomed code", actual_error_messages) + self.assertIn( + "extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code is not a valid snomed code", + actual_error_messages, + ) def test_pre_validate_extension_to_extract_the_coding_code_value(self): "Test the array length for extension and it should be length 1" invalid_json_data = deepcopy(self.json_data) # Adding a new SNOMED code and testing if a specific code is retrieved - invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"].append({ - "system": "http://snomed.info/sct", - "code": "1324681000000102", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - }) + invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"].append( + { + "system": "http://snomed.info/sct", + "code": "1324681000000102", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", + } + ) actual_value = get_generic_extension_value( invalid_json_data, "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", "http://snomed.info/sct", - "code" + "code", ) self.assertIn("1324681000000101", actual_value) # Updating system and adding another SNOMED code to verify the updated value invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"][0]["system"] = "http://xyz.info/sct" - invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"].append({ - "system": "http://snomed.info/sct", - "code": "1324681000000103", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - }) + invalid_json_data["extension"][0]["valueCodeableConcept"]["coding"].append( + { + "system": "http://snomed.info/sct", + "code": "1324681000000103", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", + } + ) actual_value = get_generic_extension_value( invalid_json_data, "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", "http://snomed.info/sct", - "code" + "code", ) self.assertIn("1324681000000102", actual_value) @@ -821,11 +873,7 @@ def test_pre_validate_protocol_applied_dose_number_positive_int(self): rejects invalid values """ for value in range(1, PreValidators.DOSE_NUMBER_MAX_VALUE + 1): - data = { - "protocolApplied": [ - {"doseNumberPositiveInt": value} - ] - } + data = {"protocolApplied": [{"doseNumberPositiveInt": value}]} validator = PreValidators(data) # Should not raise validator.pre_validate_dose_number_positive_int(data) @@ -833,11 +881,7 @@ def test_pre_validate_protocol_applied_dose_number_positive_int(self): def test_out_of_range_dose_number(self): # Invalid: doseNumberPositiveInt < 1 or > 9 for value in [0, PreValidators.DOSE_NUMBER_MAX_VALUE + 1, -1]: - data = { - "protocolApplied": [ - {"doseNumberPositiveInt": value} - ] - } + data = {"protocolApplied": [{"doseNumberPositiveInt": value}]} validator = PreValidators(data) with self.assertRaises(ValueError): validator.pre_validate_dose_number_positive_int(data) @@ -1060,8 +1104,7 @@ def test_pre_validate_disease_type_coding_codes(self): # Test data with single disease_type_coding_code ValidatorModelTests.test_string_value( self, - field_location="protocolApplied[0].targetDisease[0]." - + "coding[?(@.system=='http://snomed.info/sct')].code", + field_location="protocolApplied[0].targetDisease[0]." + "coding[?(@.system=='http://snomed.info/sct')].code", valid_strings_to_test=[ "840539006", "6142004", @@ -1094,7 +1137,10 @@ def test_pre_validate_lot_number(self): ValidatorModelTests.test_string_value( self, field_location="lotNumber", - valid_strings_to_test=["sample", ValidValues.for_strings_with_any_length_chars], + valid_strings_to_test=[ + "sample", + ValidValues.for_strings_with_any_length_chars, + ], invalid_strings_to_test=["", None, 42, 3.889], ) @@ -1164,6 +1210,7 @@ def test_pre_validate_dose_quantity_value(self): Decimal("1.123456789"), # 9 decimal place ], ) + def test_pre_validate_dose_quantity_system(self): """Test pre_validate_dose_quantity_system accepts valid values and rejects invalid values""" @@ -1192,7 +1239,7 @@ def test_pre_validate_dose_quantity_system_and_code(self): valid_json_data=deepcopy(self.json_data), field_location=field_location, invalid_value=InvalidValues.invalid_dose_quantity, - expected_error_message="If doseQuantity.code is present, doseQuantity.system must also be present" + expected_error_message="If doseQuantity.code is present, doseQuantity.system must also be present", ) def test_pre_validate_dose_quantity_unit(self): @@ -1209,7 +1256,12 @@ def test_pre_validate_reason_code_codings(self): ValidatorModelTests.test_list_value( self, field_location=f"reasonCode[{i}].coding", - valid_lists_to_test=[[{"code": "ABC123", "display": "test"}, {"code": "ABC123", "display": "test"}]], + valid_lists_to_test=[ + [ + {"code": "ABC123", "display": "test"}, + {"code": "ABC123", "display": "test"}, + ] + ], valid_list_element={"code": "ABC123", "display": "test"}, ) @@ -1253,7 +1305,14 @@ def test_pre_validate_location_identifier_system(self): def test_pre_validate_vaccine_code(self): """Test pre_validate_vaccine_code accepts valid values and rejects invalid values for vaccineCode.coding[0].code""" invalid_json_data = deepcopy(self.json_data) - test_values = ["12345abc", "12345", "1234567890123456789", "12345671", "1324681000000111", "0101291008"] + test_values = [ + "12345abc", + "12345", + "1234567890123456789", + "12345671", + "1324681000000111", + "0101291008", + ] for values in test_values: invalid_json_data["vaccineCode"]["coding"][0]["code"] = values @@ -1262,7 +1321,10 @@ def test_pre_validate_vaccine_code(self): full_error_message = str(error.exception) actual_error_messages = full_error_message.replace("Validation errors: ", "").split("; ") - self.assertIn("vaccineCode.coding[?(@.system=='http://snomed.info/sct')].code is not a valid snomed code", actual_error_messages) + self.assertIn( + "vaccineCode.coding[?(@.system=='http://snomed.info/sct')].code is not a valid snomed code", + actual_error_messages, + ) class TestImmunizationModelPreValidationRulesForReduceValidation(unittest.TestCase): diff --git a/backend/tests/test_lambda_handler.py b/backend/tests/test_lambda_handler.py index 783b160f8..9ea8bc62d 100644 --- a/backend/tests/test_lambda_handler.py +++ b/backend/tests/test_lambda_handler.py @@ -1,10 +1,12 @@ import unittest + from not_found_handler import not_found # Replace with your Lambda file path "test" + + class TestLambdaHandler(unittest.TestCase): def test_unsupported_method(self): - """Tests the function with an unsupported method (PATCH).""" event = {"httpMethod": "PATCH"} @@ -13,9 +15,7 @@ def test_unsupported_method(self): self.assertEqual(response["statusCode"], 405) - self.assertEqual( - response["headers"]["Allow"], ", ".join(["GET", "POST", "DELETE", "PUT"]) - ) # Check Allow header + self.assertEqual(response["headers"]["Allow"], ", ".join(["GET", "POST", "DELETE", "PUT"])) # Check Allow header if __name__ == "__main__": diff --git a/backend/tests/test_log_structure_wrapper.py b/backend/tests/test_log_structure_wrapper.py index 7c4e55775..5b0838563 100644 --- a/backend/tests/test_log_structure_wrapper.py +++ b/backend/tests/test_log_structure_wrapper.py @@ -1,11 +1,12 @@ -import unittest import json -from unittest.mock import patch, MagicMock, ANY +import unittest +from unittest.mock import patch + from log_structure import function_info -@patch('log_structure.firehose_logger') -@patch('log_structure.logger') +@patch("log_structure.firehose_logger") +@patch("log_structure.logger") class TestFunctionInfoWrapper(unittest.TestCase): def setUp(self): @@ -26,9 +27,9 @@ def mock_function_raises(_event, _context): def extract_all_call_args_for_logger(self, mock_logger) -> list: """Extracts all arguments for logger.*.""" return ( - [ args[0] for args, _ in mock_logger.info.call_args_list ] - + [ args[0] for args, _ in mock_logger.warning.call_args_list ] - + [ args[0] for args, _ in mock_logger.error.call_args_list ] + [args[0] for args, _ in mock_logger.info.call_args_list] + + [args[0] for args, _ in mock_logger.warning.call_args_list] + + [args[0] for args, _ in mock_logger.error.call_args_list] ) def test_successful_execution(self, mock_logger, mock_firehose_logger): @@ -42,14 +43,14 @@ def test_successful_execution(self, mock_logger, mock_firehose_logger): self.mock_redis_client.hget.return_value = "FLU" wrapped_function = function_info(self.mock_success_function) event = { - 'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, }, - 'path': test_actual_path, - 'requestContext': {'resourcePath': test_resource_path}, - 'body': "{\"identifier\": [{\"system\": \"http://test\", \"value\": \"12345\"}], \"protocolApplied\": [{\"targetDisease\": [{\"coding\": [{\"system\": \"http://snomed.info/sct\", \"code\": \"840539006\", \"display\": \"Disease caused by severe acute respiratory syndrome coronavirus 2\"}]}]}]}" + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": '{"identifier": [{"system": "http://test", "value": "12345"}], "protocolApplied": [{"targetDisease": [{"coding": [{"system": "http://snomed.info/sct", "code": "840539006", "display": "Disease caused by severe acute respiratory syndrome coronavirus 2"}]}]}]}', } # Act @@ -63,15 +64,15 @@ def test_successful_execution(self, mock_logger, mock_firehose_logger): args, kwargs = mock_logger.info.call_args logged_info = json.loads(args[0]) - self.assertIn('function_name', logged_info) - self.assertIn('time_taken', logged_info) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertEqual(logged_info['local_id'], '12345^http://test') - self.assertEqual(logged_info['vaccine_type'], 'FLU') + self.assertIn("function_name", logged_info) + self.assertIn("time_taken", logged_info) + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertEqual(logged_info["local_id"], "12345^http://test") + self.assertEqual(logged_info["vaccine_type"], "FLU") def test_successful_execution_pii(self, mock_logger, mock_firehose_logger): """Pass personally identifiable information in an event, and ensure that it is not logged anywhere.""" @@ -85,14 +86,14 @@ def test_successful_execution_pii(self, mock_logger, mock_firehose_logger): self.mock_redis_client.hget.return_value = "FLU" wrapped_function = function_info(self.mock_success_function) event = { - 'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, }, - 'path': test_actual_path, - 'requestContext': {'resourcePath': test_resource_path}, - 'body': "{\"identifier\": [{\"system\": \"http://test\", \"value\": \"12345\"}], \"contained\": [{\"resourceType\": \"Patient\", \"id\": \"Pat1\", \"identifier\": [{\"system\": \"https://fhir.nhs.uk/Id/nhs-number\", \"value\": \"9693632109\"}]}], \"protocolApplied\": [{\"targetDisease\": [{\"coding\": [{\"system\": \"http://snomed.info/sct\", \"code\": \"840539006\", \"display\": \"Disease caused by severe acute respiratory syndrome coronavirus 2\"}]}]}]}" + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": '{"identifier": [{"system": "http://test", "value": "12345"}], "contained": [{"resourceType": "Patient", "id": "Pat1", "identifier": [{"system": "https://fhir.nhs.uk/Id/nhs-number", "value": "9693632109"}]}], "protocolApplied": [{"targetDisease": [{"coding": [{"system": "http://snomed.info/sct", "code": "840539006", "display": "Disease caused by severe acute respiratory syndrome coronavirus 2"}]}]}]}', } # Act @@ -114,39 +115,42 @@ def test_exception_handling(self, mock_logger, mock_firehose_logger): self.mock_redis_client.hget.return_value = "FLU" - #Act + # Act decorated_function_raises = function_info(self.mock_function_raises) with self.assertRaises(ValueError): - #Assert - event = {'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier - }, - 'path': test_actual_path, 'requestContext': {'resourcePath': test_resource_path}, - 'body': "{\"identifier\": [{\"system\": \"http://test\", \"value\": \"12345\"}], \"protocolApplied\": [{\"targetDisease\": [{\"coding\": [{\"system\": \"http://snomed.info/sct\", \"code\": \"840539006\", \"display\": \"Disease caused by severe acute respiratory syndrome coronavirus 2\"}]}]}]}"} + # Assert + event = { + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, + }, + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": '{"identifier": [{"system": "http://test", "value": "12345"}], "protocolApplied": [{"targetDisease": [{"coding": [{"system": "http://snomed.info/sct", "code": "840539006", "display": "Disease caused by severe acute respiratory syndrome coronavirus 2"}]}]}]}', + } context = {} decorated_function_raises(event, context) - #Assert + # Assert mock_logger.exception.assert_called() mock_firehose_logger.send_log.assert_called() args, kwargs = mock_logger.exception.call_args logged_info = json.loads(args[0]) - self.assertIn('function_name', logged_info) - self.assertIn('time_taken', logged_info) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertEqual(logged_info['error'], str(ValueError("Test error"))) - self.assertEqual(logged_info['local_id'], '12345^http://test') - self.assertEqual(logged_info['vaccine_type'], 'FLU') + self.assertIn("function_name", logged_info) + self.assertIn("time_taken", logged_info) + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertEqual(logged_info["error"], str(ValueError("Test error"))) + self.assertEqual(logged_info["local_id"], "12345^http://test") + self.assertEqual(logged_info["vaccine_type"], "FLU") def test_body_missing(self, mock_logger, mock_firehose_logger): # Arrange @@ -158,29 +162,29 @@ def test_body_missing(self, mock_logger, mock_firehose_logger): wrapped_function = function_info(self.mock_success_function) event = { - 'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, }, - 'path': test_actual_path, - 'requestContext': {'resourcePath': test_resource_path}, + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, } # Act - result = wrapped_function(event, {}) + wrapped_function(event, {}) # Assert args, kwargs = mock_logger.info.call_args logged_info = json.loads(args[0]) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertNotIn('local_id', logged_info) - self.assertNotIn('vaccine_type', logged_info) + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertNotIn("local_id", logged_info) + self.assertNotIn("vaccine_type", logged_info) def test_body_not_json(self, mock_logger, mock_firehose_logger): # Arrange @@ -194,29 +198,32 @@ def test_body_not_json(self, mock_logger, mock_firehose_logger): decorated_function_raises = function_info(self.mock_function_raises) with self.assertRaises(ValueError): - #Assert - event = {'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier - }, - 'path': test_actual_path, 'requestContext': {'resourcePath': test_resource_path}, - 'body': "invalid"} + # Assert + event = { + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, + }, + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": "invalid", + } context = {} decorated_function_raises(event, context) - #Assert + # Assert args, kwargs = mock_logger.exception.call_args logged_info = json.loads(args[0]) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertNotIn('local_id', logged_info) - self.assertNotIn('vaccine_type', logged_info) + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertNotIn("local_id", logged_info) + self.assertNotIn("vaccine_type", logged_info) def test_body_invalid_identifier(self, mock_logger, mock_firehose_logger): # Arrange @@ -232,29 +239,32 @@ def test_body_invalid_identifier(self, mock_logger, mock_firehose_logger): decorated_function_raises = function_info(self.mock_function_raises) with self.assertRaises(ValueError): - #Assert - event = {'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier - }, - 'path': test_actual_path, 'requestContext': {'resourcePath': test_resource_path}, - 'body': "{\"identifier\": [], \"protocolApplied\": [{\"targetDisease\": [{\"coding\": [{\"system\": \"http://snomed.info/sct\", \"code\": \"840539006\", \"display\": \"Disease caused by severe acute respiratory syndrome coronavirus 2\"}]}]}]}"} + # Assert + event = { + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, + }, + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": '{"identifier": [], "protocolApplied": [{"targetDisease": [{"coding": [{"system": "http://snomed.info/sct", "code": "840539006", "display": "Disease caused by severe acute respiratory syndrome coronavirus 2"}]}]}]}', + } context = {} decorated_function_raises(event, context) - #Assert + # Assert args, kwargs = mock_logger.exception.call_args logged_info = json.loads(args[0]) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertNotIn('local_id', logged_info) - self.assertEqual(logged_info['vaccine_type'], 'FLU') + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertNotIn("local_id", logged_info) + self.assertEqual(logged_info["vaccine_type"], "FLU") def test_body_invalid_protocol_applied(self, mock_logger, mock_firehose_logger): # Arrange @@ -270,26 +280,29 @@ def test_body_invalid_protocol_applied(self, mock_logger, mock_firehose_logger): decorated_function_raises = function_info(self.mock_function_raises) with self.assertRaises(ValueError): - #Assert - event = {'headers': { - 'X-Correlation-ID': test_correlation, - 'X-Request-ID': test_request, - 'SupplierSystem': test_supplier - }, - 'path': test_actual_path, 'requestContext': {'resourcePath': test_resource_path}, - 'body': "{\"identifier\": [{\"system\": \"http://test\", \"value\": \"12345\"}], \"protocolApplied\": []}"} + # Assert + event = { + "headers": { + "X-Correlation-ID": test_correlation, + "X-Request-ID": test_request, + "SupplierSystem": test_supplier, + }, + "path": test_actual_path, + "requestContext": {"resourcePath": test_resource_path}, + "body": '{"identifier": [{"system": "http://test", "value": "12345"}], "protocolApplied": []}', + } context = {} decorated_function_raises(event, context) - #Assert + # Assert args, kwargs = mock_logger.exception.call_args logged_info = json.loads(args[0]) - self.assertEqual(logged_info['X-Correlation-ID'], test_correlation) - self.assertEqual(logged_info['X-Request-ID'], test_request) - self.assertEqual(logged_info['supplier'], test_supplier) - self.assertEqual(logged_info['actual_path'], test_actual_path) - self.assertEqual(logged_info['resource_path'], test_resource_path) - self.assertEqual(logged_info['local_id'], '12345^http://test') - self.assertNotIn('vaccine_type', logged_info) + self.assertEqual(logged_info["X-Correlation-ID"], test_correlation) + self.assertEqual(logged_info["X-Request-ID"], test_request) + self.assertEqual(logged_info["supplier"], test_supplier) + self.assertEqual(logged_info["actual_path"], test_actual_path) + self.assertEqual(logged_info["resource_path"], test_resource_path) + self.assertEqual(logged_info["local_id"], "12345^http://test") + self.assertNotIn("vaccine_type", logged_info) diff --git a/backend/tests/test_model_utils.py b/backend/tests/test_model_utils.py index fe6291840..a64fac812 100644 --- a/backend/tests/test_model_utils.py +++ b/backend/tests/test_model_utils.py @@ -1,9 +1,12 @@ """Tests for the utils module""" import unittest + from models.utils.generic_utils import nhs_number_mod11_check "test" + + class UtilsTests(unittest.TestCase): """Tests for models.utils.generic_utils module""" diff --git a/backend/tests/test_parameter_parser.py b/backend/tests/test_parameter_parser.py index 98660acea..8b062f147 100644 --- a/backend/tests/test_parameter_parser.py +++ b/backend/tests/test_parameter_parser.py @@ -1,9 +1,8 @@ import base64 -import unittest import datetime +import unittest from unittest.mock import create_autospec, patch -from service.fhir_service import FhirService from models.errors import ParameterException from parameter_parser import ( date_from_key, @@ -11,10 +10,11 @@ process_params, process_search_params, create_query_string, - create_query_string, include_key, SearchParams, ) +from service.fhir_service import FhirService + class TestParameterParser(unittest.TestCase): def setUp(self): @@ -43,7 +43,10 @@ def test_process_params_combines_content_and_query_string(self): processed_params = process_params(lambda_event) - expected = {self.patient_identifier_key: ["a"], self.immunization_target_key: ["b"]} + expected = { + self.patient_identifier_key: ["a"], + self.immunization_target_key: ["b"], + } self.assertEqual(expected, processed_params) @@ -58,7 +61,7 @@ def test_process_params_is_sorted(self): } processed_params = process_params(lambda_event) - for k, v in processed_params.items(): + for v in processed_params.values(): self.assertEqual(sorted(v), v) def test_process_params_does_not_process_body_on_get(self): @@ -102,13 +105,12 @@ def test_process_search_params_checks_patient_identifier_format(self): 'e.g. "https://fhir.nhs.uk/Id/nhs-number|9000000009"', ) self.mock_redis_client.hkeys.return_value = ["RSV"] - params = process_search_params( + process_search_params( { self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], self.immunization_target_key: ["RSV"], } ) - def test_process_search_params_whitelists_immunization_target(self): mock_redis_key = "RSV" @@ -117,15 +119,19 @@ def test_process_search_params_whitelists_immunization_target(self): process_search_params( { self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], - self.immunization_target_key: ["FLU", "COVID-19", "NOT-A-REAL-VALUE"], + self.immunization_target_key: [ + "FLU", + "COVID-19", + "NOT-A-REAL-VALUE", + ], } ) self.assertEqual( - str(e.exception), f"immunization-target must be one or more of the following: {mock_redis_key}", - f"Unexpected exception message: {str(e.exception)}" + str(e.exception), + f"immunization-target must be one or more of the following: {mock_redis_key}", + f"Unexpected exception message: {str(e.exception)}", ) - def test_process_search_params_immunization_target(self): mock_redis_key = "RSV" self.mock_redis_client.hkeys.return_value = [mock_redis_key] @@ -138,7 +144,6 @@ def test_process_search_params_immunization_target(self): self.assertIsNotNone(params) - def test_search_params_date_from_must_be_before_date_to(self): self.mock_redis_client.hkeys.return_value = ["RSV"] params = process_search_params( @@ -173,7 +178,10 @@ def test_search_params_date_from_must_be_before_date_to(self): } ) - self.assertEqual(str(e.exception), f"Search parameter {date_from_key} must be before {date_to_key}") + self.assertEqual( + str(e.exception), + f"Search parameter {date_from_key} must be before {date_to_key}", + ) def test_process_search_params_immunization_target_is_mandatory(self): self.mock_redis_client.hkeys.return_value = ["RSV"] @@ -183,7 +191,10 @@ def test_process_search_params_immunization_target_is_mandatory(self): self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], } ) - self.assertEqual(str(e.exception), f"Search parameter -immunization.target must have one or more values.") + self.assertEqual( + str(e.exception), + "Search parameter -immunization.target must have one or more values.", + ) def test_process_search_params_patient_identifier_is_mandatory(self): with self.assertRaises(ParameterException) as e: @@ -192,7 +203,10 @@ def test_process_search_params_patient_identifier_is_mandatory(self): self.immunization_target_key: ["a-disease-type"], } ) - self.assertEqual(str(e.exception), f"Search parameter patient.identifier must have one value.") + self.assertEqual( + str(e.exception), + "Search parameter patient.identifier must have one value.", + ) def test_create_query_string_with_all_params(self): search_params = SearchParams("a", ["b"], datetime.date(1, 2, 3), datetime.date(4, 5, 6), "c") @@ -211,22 +225,24 @@ def test_create_query_string_with_minimal_params(self): self.assertEqual(expected, query_string) - def test_create_query_string_with_multiple_immunization_targets_comma_separated(self): + def test_create_query_string_with_multiple_immunization_targets_comma_separated( + self, + ): search_params = SearchParams("a", ["b", "c"], None, None, None) query_string = create_query_string(search_params) expected = "-immunization.target=b,c&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7Ca" self.assertEqual(expected, query_string) - def test_process_search_params_dedupes_immunization_targets_and_respects_include(self): + def test_process_search_params_dedupes_immunization_targets_and_respects_include( + self, + ): """Ensure duplicate immunization targets are deduped and include is preserved.""" self.mock_redis_client.hkeys.return_value = ["RSV", "FLU"] params = process_search_params( { - self.patient_identifier_key: [ - "https://fhir.nhs.uk/Id/nhs-number|9000000009" - ], + self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], self.immunization_target_key: ["RSV", "RSV", "FLU"], "_include": ["immunization:patient"], } @@ -246,9 +262,7 @@ def test_process_search_params_aggregates_date_errors(self): with self.assertRaises(ParameterException) as e: process_search_params( { - self.patient_identifier_key: [ - "https://fhir.nhs.uk/Id/nhs-number|9000000009" - ], + self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], self.immunization_target_key: ["RSV"], self.date_from_key: ["2021-01-01", "2021-01-02"], # too many values self.date_to_key: ["invalid-date"], # invalid format @@ -269,14 +283,15 @@ def test_process_search_params_invalid_nhs_number_is_rejected(self): with self.assertRaises(ParameterException) as e: process_search_params( { - self.patient_identifier_key: [ - "https://fhir.nhs.uk/Id/nhs-number|1234567890" # invalid mod11 - ], + self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|1234567890"], # invalid mod11 self.immunization_target_key: ["RSV"], } ) - self.assertEqual(str(e.exception), f"Search parameter {self.patient_identifier_key} must be a valid NHS number.") + self.assertEqual( + str(e.exception), + f"Search parameter {self.patient_identifier_key} must be a valid NHS number.", + ) def test_process_search_params_invalid_include_value_is_rejected(self): """_include may only be 'Immunization:patient' if provided.""" @@ -285,9 +300,7 @@ def test_process_search_params_invalid_include_value_is_rejected(self): with self.assertRaises(ParameterException) as e: process_search_params( { - self.patient_identifier_key: [ - "https://fhir.nhs.uk/Id/nhs-number|9000000009" - ], + self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"], self.immunization_target_key: ["RSV"], "_include": ["Patient:practitioner"], } diff --git a/backend/tests/test_search_imms.py b/backend/tests/test_search_imms.py index 7e432758a..0041719c3 100644 --- a/backend/tests/test_search_imms.py +++ b/backend/tests/test_search_imms.py @@ -1,12 +1,12 @@ import json import unittest +from pathlib import Path from unittest.mock import create_autospec, patch +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.fhir_controller import FhirController from models.errors import Severity, Code, create_operation_outcome from search_imms_handler import search_imms -from pathlib import Path -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE script_location = Path(__file__).absolute().parent @@ -79,7 +79,11 @@ def test_search_immunizations_get_id_from_body(self): def test_search_immunizations_get_id_from_body_passing_none(self): """it should enter search_immunizations as both the request params are none""" - lambda_event = {"pathParameters": {"id": "an-id"}, "body": None, "queryStringParameters": None} + lambda_event = { + "pathParameters": {"id": "an-id"}, + "body": None, + "queryStringParameters": None, + } exp_res = {"a-key": "a-value"} self.controller.search_immunizations.return_value = exp_res diff --git a/backend/tests/test_update_imms.py b/backend/tests/test_update_imms.py index 5c1afef88..f152af5ce 100644 --- a/backend/tests/test_update_imms.py +++ b/backend/tests/test_update_imms.py @@ -1,10 +1,9 @@ import unittest from unittest.mock import create_autospec, patch +from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE from controller.fhir_controller import FhirController -from models.errors import Severity, Code, create_operation_outcome from update_imms_handler import update_imms -from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE class TestUpdateImmunizations(unittest.TestCase): @@ -20,7 +19,6 @@ def setUp(self): def tearDown(self): patch.stopall() - def test_update_immunization(self): """it should call service update method""" lambda_event = {"pathParameters": {"id": "an-id"}} @@ -42,12 +40,6 @@ def test_update_imms_exception(self, mock_create_response): error_msg = "an unhandled error" self.controller.update_immunization.side_effect = Exception(error_msg) - exp_error = create_operation_outcome( - resource_id=None, - severity=Severity.error, - code=Code.server_error, - diagnostics=GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE, - ) mock_response = "controller-response-error" mock_create_response.return_value = mock_response @@ -67,8 +59,6 @@ def test_update_imms_exception(self, mock_create_response): self.assertEqual(diagnostics, GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE) self.assertEqual(act_res, mock_response) - - def test_update_imms_with_duplicated_identifier_returns_error(self): """Should return an IdentifierDuplication error""" lambda_event = {"pathParameters": {"id": "an-id"}} diff --git a/backend/tests/test_utils.py b/backend/tests/test_utils.py index 72f9255a2..1879c58f3 100644 --- a/backend/tests/test_utils.py +++ b/backend/tests/test_utils.py @@ -1,15 +1,19 @@ """Tests for generic utils""" import unittest -from unittest.mock import patch from copy import deepcopy +from unittest.mock import patch -from models.utils.validation_utils import convert_disease_codes_to_vaccine_type, get_vaccine_type +from models.utils.validation_utils import ( + convert_disease_codes_to_vaccine_type, + get_vaccine_type, +) from testing_utils.generic_utils import load_json_data, update_target_disease_code class TestGenericUtils(unittest.TestCase): """Tests for generic utils functions""" + def setUp(self): """Set up for each test. This runs before every test""" self.json_data = load_json_data(filename="completed_mmr_immunization_event.json") @@ -31,9 +35,17 @@ def test_convert_disease_codes_to_vaccine_type_returns_vaccine_type(self): (["14189004", "36989005", "36653000"], "MMR"), (["36989005", "14189004", "36653000"], "MMR"), (["36653000", "14189004", "36989005"], "MMR"), - (["55735004"], "RSV") + (["55735004"], "RSV"), + ] + self.mock_redis_client.hget.side_effect = [ + "COVID19", + "FLU", + "HPV", + "MMR", + "MMR", + "MMR", + "RSV", ] - self.mock_redis_client.hget.side_effect = ['COVID19', 'FLU', 'HPV', 'MMR', 'MMR', 'MMR', 'RSV'] for combination, vaccine_type in valid_combinations: self.assertEqual(convert_disease_codes_to_vaccine_type(combination), vaccine_type) @@ -59,20 +71,28 @@ def test_get_vaccine_type(self): Test that get_vaccine_type returns the correct vaccine type when given valid json data with a valid combination of target disease code, or raises an appropriate error otherwise """ - self.mock_redis_client.hget.return_value = 'RSV' + self.mock_redis_client.hget.return_value = "RSV" # TEST VALID DATA - valid_json_data = load_json_data(filename=f"completed_rsv_immunization_event.json") + valid_json_data = load_json_data(filename="completed_rsv_immunization_event.json") vac_type = get_vaccine_type(valid_json_data) self.assertEqual(vac_type, "RSV") self.mock_redis_client.hget.return_value = "FLU" # VALID DATA: coding field with multiple coding systems including SNOMED - flu_json_data = load_json_data(filename=f"completed_flu_immunization_event.json") + flu_json_data = load_json_data(filename="completed_flu_immunization_event.json") valid_target_disease_element = { "coding": [ - {"system": "ANOTHER_SYSTEM_URL", "code": "ANOTHER_CODE", "display": "Influenza"}, - {"system": "http://snomed.info/sct", "code": "6142004", "display": "Influenza"}, + { + "system": "ANOTHER_SYSTEM_URL", + "code": "ANOTHER_CODE", + "display": "Influenza", + }, + { + "system": "http://snomed.info/sct", + "code": "6142004", + "display": "Influenza", + }, ] } flu_json_data["protocolApplied"][0]["targetDisease"][0] = valid_target_disease_element @@ -80,9 +100,7 @@ def test_get_vaccine_type(self): # TEST INVALID DATA FOR SINGLE TARGET DISEASE self.mock_redis_client.hget.return_value = None # Reset mock for invalid cases - covid_19_json_data = load_json_data( - filename=f"completed_covid19_immunization_event.json" - ) + covid_19_json_data = load_json_data(filename="completed_covid19_immunization_event.json") # INVALID DATA, SINGLE TARGET DISEASE: No targetDisease field invalid_covid_19_json_data = deepcopy(covid_19_json_data) @@ -99,7 +117,15 @@ def test_get_vaccine_type(self): # INVALID DATA, SINGLE TARGET DISEASE: No "coding" field {"text": "Influenza"}, # INVALID DATA, SINGLE TARGET DISEASE: Valid code, but no snomed coding system - {"coding": [{"system": "NOT_THE_SNOMED_URL", "code": "6142004", "display": "Influenza"}]}, + { + "coding": [ + { + "system": "NOT_THE_SNOMED_URL", + "code": "6142004", + "display": "Influenza", + } + ] + }, # INVALID DATA, SINGLE TARGET DISEASE: coding field doesn't contain a code {"coding": [{"system": "http://snomed.info/sct", "display": "Influenza"}]}, ] @@ -126,7 +152,7 @@ def test_get_vaccine_type(self): ) # TEST INVALID DATA FOR MULTIPLE TARGET DISEASES - mmr_json_data = load_json_data(filename=f"completed_mmr_immunization_event.json") + mmr_json_data = load_json_data(filename="completed_mmr_immunization_event.json") # INVALID DATA, MULTIPLE TARGET DISEASES: Invalid code combination invalid_mmr_json_data = deepcopy(mmr_json_data) @@ -145,7 +171,15 @@ def test_get_vaccine_type(self): # INVALID DATA, MULTIPLE TARGET DISEASES: No "coding" field {"text": "Mumps"}, # INVALID DATA, MULTIPLE TARGET DISEASES: Valid code, but no snomed coding system - {"coding": [{"system": "NOT_THE_SNOMED_URL", "code": "36989005", "display": "Mumps"}]}, + { + "coding": [ + { + "system": "NOT_THE_SNOMED_URL", + "code": "36989005", + "display": "Mumps", + } + ] + }, # INVALID DATA, MULTIPLE TARGET DISEASES: coding field doesn't contain a code {"coding": [{"system": "http://snomed.info/sct", "display": "Mumps"}]}, ] diff --git a/backend/tests/test_validation_utils.py b/backend/tests/test_validation_utils.py index 6ec795ac3..c5914b2a1 100644 --- a/backend/tests/test_validation_utils.py +++ b/backend/tests/test_validation_utils.py @@ -2,6 +2,8 @@ from copy import deepcopy from jsonpath_ng.ext import parse + +from models.fhir_immunization import ImmunizationValidator from models.obtain_field_value import ObtainFieldValue from models.utils.generic_utils import ( get_current_name_instance, @@ -9,8 +11,6 @@ patient_and_practitioner_value_and_index, obtain_name_field_location, ) - -from models.fhir_immunization import ImmunizationValidator from testing_utils.generic_utils import ( load_json_data, ) @@ -28,7 +28,8 @@ def setUp(self): deepcopy(self.json_data), ValidValues.valid_name_4_instances ) self.updated_PatientandPractitioner_json = parse("contained[?(@.resourceType=='Practitioner')].name").update( - deepcopy(self.updated_json_data), ValidValues.valid_name_4_instances_practitioner + deepcopy(self.updated_json_data), + ValidValues.valid_name_4_instances_practitioner, ) def test_get_current_name_instance_multiple_names(self): @@ -51,7 +52,10 @@ def test_get_current_name_instance_multiple_names(self): ), # Two name instances with no "use" or period, returns first name instance index 0 ( - [NameInstances.ValidCurrent.given_and_family_only, NameInstances.ValidCurrent.given_and_family_only], + [ + NameInstances.ValidCurrent.given_and_family_only, + NameInstances.ValidCurrent.given_and_family_only, + ], ValidValues.occurrenceDateTime, 0, NameInstances.ValidCurrent.given_and_family_only, @@ -70,7 +74,10 @@ def test_get_current_name_instance_multiple_names(self): ), # Four invalid name instances, name instance containing family and given returned index 0 ( - [InvalidValues.name_with_missing_values[0], InvalidValues.name_with_missing_values[1]], + [ + InvalidValues.name_with_missing_values[0], + InvalidValues.name_with_missing_values[1], + ], ValidValues.occurrenceDateTime, 0, InvalidValues.name_with_missing_values[0], @@ -171,7 +178,13 @@ def test_patient_and_practitioner_value_and_index(self): (invalid_json_data, "family", "Practitioner", "Nightingale", 3), ] - for imms, name_value, resource_type, expected_name, expected_index in test_cases: + for ( + imms, + name_value, + resource_type, + expected_name, + expected_index, + ) in test_cases: name_field, index = patient_and_practitioner_value_and_index(imms, name_value, resource_type) self.assertEqual(name_field, expected_name) self.assertEqual(index, expected_index) @@ -208,13 +221,43 @@ def test_obtain_name_field_location(self): test_cases = [ # Four patient and practitioner name instances, name instance containing the "current" name index 1 returned - (valid_json_data, "given", "Patient", "contained[?(@.resourceType=='Patient')].name[1].given"), - (valid_json_data, "family", "Patient", "contained[?(@.resourceType=='Patient')].name[1].family"), - (valid_json_data, "given", "Practitioner", "contained[?(@.resourceType=='Practitioner')].name[1].given"), - (valid_json_data, "family", "Practitioner", "contained[?(@.resourceType=='Practitioner')].name[1].family"), + ( + valid_json_data, + "given", + "Patient", + "contained[?(@.resourceType=='Patient')].name[1].given", + ), + ( + valid_json_data, + "family", + "Patient", + "contained[?(@.resourceType=='Patient')].name[1].family", + ), + ( + valid_json_data, + "given", + "Practitioner", + "contained[?(@.resourceType=='Practitioner')].name[1].given", + ), + ( + valid_json_data, + "family", + "Practitioner", + "contained[?(@.resourceType=='Practitioner')].name[1].family", + ), # One name instance, first name instance index 0 returned - (valid_json_data_single, "given", "Patient", "contained[?(@.resourceType=='Patient')].name[0].given"), - (valid_json_data_single, "family", "Patient", "contained[?(@.resourceType=='Patient')].name[0].family"), + ( + valid_json_data_single, + "given", + "Patient", + "contained[?(@.resourceType=='Patient')].name[0].given", + ), + ( + valid_json_data_single, + "family", + "Patient", + "contained[?(@.resourceType=='Patient')].name[0].family", + ), ( valid_json_data_single, "given", diff --git a/backend/tests/testing_utils/generic_utils.py b/backend/tests/testing_utils/generic_utils.py index d39d48176..e8aff0ea4 100644 --- a/backend/tests/testing_utils/generic_utils.py +++ b/backend/tests/testing_utils/generic_utils.py @@ -3,12 +3,13 @@ import json import os import unittest +from datetime import datetime, date from decimal import Decimal from typing import Literal, Any -from jsonpath_ng.ext import parse -from datetime import datetime, date from typing import Union, List +from jsonpath_ng.ext import parse + def load_json_data(filename: str): """Load the json data""" @@ -84,7 +85,10 @@ def test_invalid_values_rejected( def update_contained_resource_field( - json_data: dict, resource_to_update: Literal["Patient", "Practitioner"], field_to_update: str, update_value: Any + json_data: dict, + resource_to_update: Literal["Patient", "Practitioner"], + field_to_update: str, + update_value: Any, ) -> dict: """ Updates the field of the given resource within the contained resources of the json data @@ -94,6 +98,7 @@ def update_contained_resource_field( ) return json_data + def format_date_types(dates: List[Union[date, datetime]], mode: str = "auto") -> List[str]: """ Accepts a list of date or datetime objects and returns them as strings: @@ -106,7 +111,7 @@ def format_date_types(dates: List[Union[date, datetime]], mode: str = "auto") -> if mode == "datetime": formatted.append(future_date.isoformat()) # full datetime with timezone elif mode == "date": - formatted.append(future_date.strftime('%Y-%m-%d')) # just date + formatted.append(future_date.strftime("%Y-%m-%d")) # just date else: raise TypeError(f"Unsupported type {type(future_date)}; expected date or datetime.") diff --git a/backend/tests/testing_utils/immunization_utils.py b/backend/tests/testing_utils/immunization_utils.py index 1704b6165..f25f69a22 100644 --- a/backend/tests/testing_utils/immunization_utils.py +++ b/backend/tests/testing_utils/immunization_utils.py @@ -2,8 +2,8 @@ from fhir.resources.R4B.immunization import Immunization -from testing_utils.values_for_tests import ValidValues from testing_utils.generic_utils import load_json_data +from testing_utils.values_for_tests import ValidValues VALID_NHS_NUMBER = ValidValues.nhs_number @@ -14,7 +14,9 @@ def create_covid_19_immunization(imms_id, nhs_number=VALID_NHS_NUMBER) -> Immuni def create_covid_19_immunization_dict( - imms_id, nhs_number=VALID_NHS_NUMBER, occurrence_date_time="2021-02-07T13:28:17+00:00" + imms_id, + nhs_number=VALID_NHS_NUMBER, + occurrence_date_time="2021-02-07T13:28:17+00:00", ): immunization_json = load_json_data("completed_covid19_immunization_event.json") immunization_json["id"] = imms_id diff --git a/backend/tests/testing_utils/mandation_test_utils.py b/backend/tests/testing_utils/mandation_test_utils.py index 067a673ae..d5a21d39c 100644 --- a/backend/tests/testing_utils/mandation_test_utils.py +++ b/backend/tests/testing_utils/mandation_test_utils.py @@ -1,10 +1,9 @@ """Mandation test utilities""" import unittest -from copy import deepcopy -from pydantic import ValidationError from jsonpath_ng.ext import parse +from pydantic import ValidationError class MandationTests: @@ -61,16 +60,16 @@ def test_missing_mandatory_field_rejected( # Set the expected error message expected_error_message = ( - expected_error_message if expected_error_message else f"Validation errors: {field_location} is a mandatory field" + expected_error_message + if expected_error_message + else f"Validation errors: {field_location} is a mandatory field" ) if is_mandatory_fhir: # Test that correct error message is raised with test_instance.assertRaises(ValidationError) as error: test_instance.validator.validate(invalid_json_data) - test_instance.assertTrue( - (expected_error_message + f" (type={expected_error_type})") in str(error.exception) - ) + test_instance.assertTrue((expected_error_message + f" (type={expected_error_type})") in str(error.exception)) else: # Test that correct error message is raised diff --git a/backend/tests/testing_utils/pre_validation_test_utils.py b/backend/tests/testing_utils/pre_validation_test_utils.py index 4c287bbc3..9f8d5f36b 100644 --- a/backend/tests/testing_utils/pre_validation_test_utils.py +++ b/backend/tests/testing_utils/pre_validation_test_utils.py @@ -2,9 +2,9 @@ import unittest from copy import deepcopy -from decimal import Decimal from jsonpath_ng.ext import parse + from .generic_utils import ( test_valid_values_accepted, test_invalid_values_rejected, @@ -197,8 +197,7 @@ def test_list_value( valid_json_data, field_location=field_location, invalid_value=invalid_length_list, - expected_error_message=f"{field_location} must be an array of length " - + f"{predefined_list_length}", + expected_error_message=f"{field_location} must be an array of length " + f"{predefined_list_length}", ) else: test_invalid_values_rejected( @@ -322,8 +321,8 @@ def test_date_value( valid_json_data, field_location=field_location, invalid_value=invalid_date_format, - expected_error_message=f"{field_location} must not be in the future", - ) + expected_error_message=f"{field_location} must not be in the future", + ) @staticmethod def test_date_time_value( @@ -347,12 +346,12 @@ def test_date_time_value( ) if is_occurrence_date_time: - expected_error_message += ( - "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n" - f"Note that partial dates are not allowed for {field_location} in this service.\n" - ) - valid_datetime_formats = ValidValues.for_date_times_strict_timezones - invalid_datetime_formats = InvalidValues.for_date_time_string_formats_for_strict_timezone + expected_error_message += ( + "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n" + f"Note that partial dates are not allowed for {field_location} in this service.\n" + ) + valid_datetime_formats = ValidValues.for_date_times_strict_timezones + invalid_datetime_formats = InvalidValues.for_date_time_string_formats_for_strict_timezone else: # For recorded, skip values that are valid ISO with non-restricted timezone valid_datetime_formats = ValidValues.for_date_times_relaxed_timezones diff --git a/backend/tests/testing_utils/test_utils_for_batch.py b/backend/tests/testing_utils/test_utils_for_batch.py index c15b5465b..ce7ee25ef 100644 --- a/backend/tests/testing_utils/test_utils_for_batch.py +++ b/backend/tests/testing_utils/test_utils_for_batch.py @@ -8,9 +8,22 @@ class ForwarderValues: "ENVIRONMENT": "internal-dev-test", } - EXPECTED_KEYS = ["file_key", "row_id", "created_at_formatted_string", "local_id", "imms_id", "operation_requested"] - - EXPECTED_KEYS_DIAGNOSTICS = ["file_key", "row_id", "created_at_formatted_string", "local_id", "diagnostics"] + EXPECTED_KEYS = [ + "file_key", + "row_id", + "created_at_formatted_string", + "local_id", + "imms_id", + "operation_requested", + ] + + EXPECTED_KEYS_DIAGNOSTICS = [ + "file_key", + "row_id", + "created_at_formatted_string", + "local_id", + "diagnostics", + ] EXPECTED_VALUES = { "file_key": "test_file_key", @@ -120,11 +133,24 @@ class MockFhirImmsResources: "recorded": "2024-09-04", "primarySource": True, "manufacturer": {"display": "Sanofi Pasteur"}, - "location": {"identifier": {"value": "RJC02", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}}, + "location": { + "identifier": { + "value": "RJC02", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + } + }, "lotNumber": "BN92478105653", "expirationDate": "2024-09-15", "site": {"coding": [{"system": Urls.SNOMED, "code": "368209003", "display": "Right arm"}]}, - "route": {"coding": [{"system": Urls.SNOMED, "code": "1210999013", "display": "Intradermal use"}]}, + "route": { + "coding": [ + { + "system": Urls.SNOMED, + "code": "1210999013", + "display": "Intradermal use", + } + ] + }, "doseQuantity": { "value": 0.3, "unit": "Inhalation - unit of product usage", @@ -135,7 +161,10 @@ class MockFhirImmsResources: { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "RVVKC"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RVVKC", + }, } }, {"actor": {"reference": "#Practitioner1"}}, @@ -180,10 +209,5 @@ class MockFhirImmsResources: } ], "recorded": "2024-09-04", - "identifier": [ - { - "value": "RSV_002", - "system": "https://www.ravs.england.nhs.uk/" - } - ] + "identifier": [{"value": "RSV_002", "system": "https://www.ravs.england.nhs.uk/"}], } diff --git a/backend/tests/testing_utils/values_for_tests.py b/backend/tests/testing_utils/values_for_tests.py index 7ddec81af..ed32e990c 100644 --- a/backend/tests/testing_utils/values_for_tests.py +++ b/backend/tests/testing_utils/values_for_tests.py @@ -1,10 +1,10 @@ """Store values for use in tests""" from dataclasses import dataclass -from decimal import Decimal -from .generic_utils import format_date_types from datetime import datetime, timedelta +from decimal import Decimal +from .generic_utils import format_date_types # Lists of data types for 'invalid data type' testing integers = [-1, 0, 1] @@ -56,13 +56,27 @@ class ValidValues: ) # Not a valid snomed code, but is valid coding format for format testing - snomed_coding_element = {"system": "http://snomed.info/sct", "code": "ABC123", "display": "test"} + snomed_coding_element = { + "system": "http://snomed.info/sct", + "code": "ABC123", + "display": "test", + } valid_dose_quantity = [ - {"value": 3, "unit": "milliliter", "system": "http://unitsofmeasure.org", "code": "ml"}, - {"value": 2, "unit": "ml", "system": "http://snomed.info/sct", "code": "258773002"}, + { + "value": 3, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml", + }, + { + "value": 2, + "unit": "ml", + "system": "http://snomed.info/sct", + "code": "258773002", + }, {"value": 4, "unit": "ml", "system": "http://snomed.info/sct"}, - {"value": 5, "unit": "ml" } + {"value": 5, "unit": "ml"}, ] manufacturer_resource_id_Man1 = {"resourceType": "Manufacturer", "id": "Man1"} @@ -75,20 +89,39 @@ class ValidValues: patient_resource_id_Pat2 = {"resourceType": "Patient", "id": "Pat2"} - questionnnaire_resource_id_QR1 = {"resourceType": "QuestionnaireResponse", "id": "QR1", "status": "completed"} + questionnnaire_resource_id_QR1 = { + "resourceType": "QuestionnaireResponse", + "id": "QR1", + "status": "completed", + } - questionnaire_immunisation = {"linkId": "Immunisation", "answer": [{"valueReference": {"reference": "#"}}]} + questionnaire_immunisation = { + "linkId": "Immunisation", + "answer": [{"valueReference": {"reference": "#"}}], + } - questionnaire_reduce_validation_true = {"linkId": "ReduceValidation", "answer": [{"valueBoolean": True}]} + questionnaire_reduce_validation_true = { + "linkId": "ReduceValidation", + "answer": [{"valueBoolean": True}], + } - questionnaire_reduce_validation_false = {"linkId": "ReduceValidation", "answer": [{"valueBoolean": False}]} + questionnaire_reduce_validation_false = { + "linkId": "ReduceValidation", + "answer": [{"valueBoolean": False}], + } - questionnaire_ip_address = {"linkId": "IpAddress", "answer": [{"valueString": "IP_ADDRESS"}]} + questionnaire_ip_address = { + "linkId": "IpAddress", + "answer": [{"valueString": "IP_ADDRESS"}], + } performer_actor_organization = { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, "display": "Acme Healthcare", } } @@ -97,7 +130,10 @@ class ValidValues: performer_actor_reference_internal_Pract2 = {"actor": {"reference": "#Pract2"}} - performer = [{"actor": {"reference": "#Pract1"}}, {"actor": {"type": "Organization", "display": "Acme Healthcare"}}] + performer = [ + {"actor": {"reference": "#Pract1"}}, + {"actor": {"type": "Organization", "display": "Acme Healthcare"}}, + ] vaccination_procedure_coding_with_one_snomed_code = [ { @@ -124,7 +160,11 @@ class ValidValues: ] dummy_coding_with_one_snomed_code = [ - {"system": "http://snomed.info/sct", "code": "DUMMY CODE 1", "display": "DUMMY TERM 1"}, + { + "system": "http://snomed.info/sct", + "code": "DUMMY CODE 1", + "display": "DUMMY TERM 1", + }, ] vaccination_procedure_with_one_snomed_code = { @@ -173,7 +213,10 @@ class ValidValues: "use": "official", "family": "Taylor", "given": ["Sarah"], - "period": {"start": date_before_occurenceDateTime, "end": date_after_occurenceDateatetime}, + "period": { + "start": date_before_occurenceDateTime, + "end": date_after_occurenceDateatetime, + }, }, {"family": "Taylor", "given": ["Sar"]}, {"use": "old", "family": "Tray", "given": ["Sarah"]}, @@ -190,7 +233,10 @@ class ValidValues: "use": "official", "family": "Night", "given": ["Florence"], - "period": {"start": date_before_occurenceDateTime, "end": date_after_occurenceDateatetime}, + "period": { + "start": date_before_occurenceDateTime, + "end": date_after_occurenceDateatetime, + }, }, {"family": "Night", "given": ["Florence"]}, {"use": "old", "family": "Tray", "given": ["Florence"]}, @@ -205,7 +251,11 @@ class ValidCurrent: given_and_family_only = {"given": ["a_given_name"], "family": "a_family_name"} - with_use_official = {"given": ["a_given_name"], "family": "a_family_name", "use": "official"} + with_use_official = { + "given": ["a_given_name"], + "family": "a_family_name", + "use": "official", + } with_period_start = { "given": ["a_given_name"], @@ -234,7 +284,11 @@ class ValidCurrent: class ValidNonCurrent: """Name instances which are valid but not current""" - before_period_start = {"given": ["a_given_name"], "family": "a_family_name", "period": {"start": "2100-01-01"}} + before_period_start = { + "given": ["a_given_name"], + "family": "a_family_name", + "period": {"start": "2100-01-01"}, + } after_period_end = { "given": ["a_given_name"], @@ -249,7 +303,10 @@ class Invalid: given_name_only = {"given": ["a_given_name"]} - family_name_only_with_use_official = {"family": "a_family_name", "use": "official"} + family_name_only_with_use_official = { + "family": "a_family_name", + "use": "official", + } family_name_only_with_use_official_and_period_start_and_end = { "family": "a_family_name", @@ -298,21 +355,21 @@ class InvalidValues: sample_inputs = [ now + timedelta(days=1), now + timedelta(days=365), - now + timedelta(days=730) + now + timedelta(days=730), ] - for_future_dates = format_date_types(sample_inputs, mode = "date") + for_future_dates = format_date_types(sample_inputs, mode="date") # Strings which are not in acceptable date time format for_date_time_string_formats_for_relaxed_timezone = [ "", # Empty string "invalid", # Invalid format - *format_date_types(sample_inputs, mode = "datetime"), + *format_date_types(sample_inputs, mode="datetime"), "20000101", # Date digits only (i.e. without hypens) "20000101000000", # Date and time digits only "200001010000000000", # Date, time and timezone digits only - "2000-01-01T10:34:27", # Date with Time only - "2000-01-01T10:34:27.234", # Date with Time and milliseconds + "2000-01-01T10:34:27", # Date with Time only + "2000-01-01T10:34:27.234", # Date with Time and milliseconds "2000", # Year only "2000-01", # Year and month only "2000-01-01T00:00:00+00", # Date and time with GMT timezone offset only in hours @@ -375,8 +432,16 @@ class InvalidValues: practitioner_resource_with_no_id = {"resourceType": "Practitioner"} dummy_coding_with_two_snomed_codes = [ - {"system": "http://snomed.info/sct", "code": "DUMMY SNOMED CODE 1", "display": "DUMMY SNOMED TERM 1"}, - {"system": "http://snomed.info/sct", "code": "DUMMY SNOMED CODE 2", "display": "DUMMY SNOMED TERM 2"}, + { + "system": "http://snomed.info/sct", + "code": "DUMMY SNOMED CODE 1", + "display": "DUMMY SNOMED TERM 1", + }, + { + "system": "http://snomed.info/sct", + "code": "DUMMY SNOMED CODE 2", + "display": "DUMMY SNOMED TERM 2", + }, ] vaccination_situation_with_two_snomed_codes = { diff --git a/batch_processor_filter/README.md b/batch_processor_filter/README.md index 9c6c5b9ea..6ed81a86a 100644 --- a/batch_processor_filter/README.md +++ b/batch_processor_filter/README.md @@ -1,20 +1,24 @@ # batch-processor-filter-lambda ## Contributing to this project + You will first need to have the required setup as specified in the root README. -Then, getting set up to contribute to this Lambda is the same for any others. Refer to +Then, getting set up to contribute to this Lambda is the same for any others. Refer to the `Setting up a virtual environment with poetry` section in the root README. Assuming 1 and 2 have already been done, then you will just need to follow steps 3-5 for this specific directory. Then run: + ``` make test ``` + to verify your setup. ## Overview -The context of this Lambda function should be understood within the following architecture diagram: + +The context of this Lambda function should be understood within the following architecture diagram: https://nhsd-confluence.digital.nhs.uk/spaces/Vacc/pages/1161762503/Immunisation+FHIR+API+-+Batch+Ingestion+Improvements The purpose of the batch-processor-filter Lambda function is to ensure that there is only ever one batch file event diff --git a/batch_processor_filter/src/batch_audit_repository.py b/batch_processor_filter/src/batch_audit_repository.py index aa1f397bf..4f0f3ef4a 100644 --- a/batch_processor_filter/src/batch_audit_repository.py +++ b/batch_processor_filter/src/batch_audit_repository.py @@ -1,12 +1,19 @@ import boto3 from boto3.dynamodb.conditions import Key -from constants import AUDIT_TABLE_NAME, REGION_NAME, AUDIT_TABLE_FILENAME_GSI, AuditTableKeys, FileStatus, \ - AUDIT_TABLE_QUEUE_NAME_GSI +from constants import ( + AUDIT_TABLE_NAME, + REGION_NAME, + AUDIT_TABLE_FILENAME_GSI, + AuditTableKeys, + FileStatus, + AUDIT_TABLE_QUEUE_NAME_GSI, +) class BatchAuditRepository: """Batch audit repository class.""" + _DUPLICATE_CHECK_FILE_STATUS_CONDITION = ( Key(AuditTableKeys.STATUS).eq(FileStatus.PROCESSED) | Key(AuditTableKeys.STATUS).eq(FileStatus.PREPROCESSED) @@ -21,7 +28,7 @@ def is_duplicate_file(self, file_key: str) -> bool: matching_files = self._batch_audit_table.query( IndexName=AUDIT_TABLE_FILENAME_GSI, KeyConditionExpression=Key(AuditTableKeys.FILENAME).eq(file_key), - FilterExpression=self._DUPLICATE_CHECK_FILE_STATUS_CONDITION + FilterExpression=self._DUPLICATE_CHECK_FILE_STATUS_CONDITION, ).get("Items", []) return len(matching_files) > 0 @@ -32,8 +39,8 @@ def is_event_processing_or_failed_for_supplier_and_vacc_type(self, supplier: str for status in self._PROCESSING_AND_FAILED_STATUSES: files_in_queue = self._batch_audit_table.query( IndexName=AUDIT_TABLE_QUEUE_NAME_GSI, - KeyConditionExpression=Key(AuditTableKeys.QUEUE_NAME).eq(queue_name) & Key(AuditTableKeys.STATUS) - .eq(status) + KeyConditionExpression=Key(AuditTableKeys.QUEUE_NAME).eq(queue_name) + & Key(AuditTableKeys.STATUS).eq(status), ).get("Items", []) if len(files_in_queue) > 0: @@ -47,5 +54,5 @@ def update_status(self, message_id: str, updated_status: str) -> None: UpdateExpression="SET #status = :status", ExpressionAttributeNames={"#status": "status"}, ExpressionAttributeValues={":status": updated_status}, - ConditionExpression="attribute_exists(message_id)" + ConditionExpression="attribute_exists(message_id)", ) diff --git a/batch_processor_filter/src/batch_file_repository.py b/batch_processor_filter/src/batch_file_repository.py index 198eb0691..239016e10 100644 --- a/batch_processor_filter/src/batch_file_repository.py +++ b/batch_processor_filter/src/batch_file_repository.py @@ -1,4 +1,5 @@ """Module for the batch file repository""" + from csv import writer from io import StringIO, BytesIO @@ -10,15 +11,18 @@ class BatchFileRepository: """Repository class to handle interactions with batch files e.g. management of the source and ack files""" + _ARCHIVE_FILE_DIR: str = "archive" _SOURCE_BUCKET_NAME: str = SOURCE_BUCKET_NAME _ACK_BUCKET_NAME: str = ACK_BUCKET_NAME def __init__(self): - self._s3_client = boto3.client('s3') + self._s3_client = boto3.client("s3") @staticmethod - def _create_ack_failure_data(batch_file_created_event: BatchFileCreatedEvent) -> dict: + def _create_ack_failure_data( + batch_file_created_event: BatchFileCreatedEvent, + ) -> dict: return { "MESSAGE_HEADER_ID": batch_file_created_event["message_id"], "HEADER_RESPONSE_CODE": "Failure", @@ -38,15 +42,17 @@ def move_source_file_to_archive(self, file_key: str) -> None: self._s3_client.copy_object( Bucket=self._SOURCE_BUCKET_NAME, CopySource={"Bucket": self._SOURCE_BUCKET_NAME, "Key": file_key}, - Key=f"{self._ARCHIVE_FILE_DIR}/{file_key}" + Key=f"{self._ARCHIVE_FILE_DIR}/{file_key}", ) self._s3_client.delete_object(Bucket=self._SOURCE_BUCKET_NAME, Key=file_key) def upload_failure_ack(self, batch_file_created_event: BatchFileCreatedEvent) -> None: ack_failure_data = self._create_ack_failure_data(batch_file_created_event) - ack_filename = ("ack/" + batch_file_created_event["filename"] - .replace(".csv", f"_InfAck_{batch_file_created_event['created_at_formatted_string']}.csv")) + ack_filename = "ack/" + batch_file_created_event["filename"].replace( + ".csv", + f"_InfAck_{batch_file_created_event['created_at_formatted_string']}.csv", + ) # Create CSV file with | delimiter, filetype .csv csv_buffer = StringIO() diff --git a/batch_processor_filter/src/batch_processor_filter_service.py b/batch_processor_filter/src/batch_processor_filter_service.py index cf530b244..7f4bcfe0b 100644 --- a/batch_processor_filter/src/batch_processor_filter_service.py +++ b/batch_processor_filter/src/batch_processor_filter_service.py @@ -1,26 +1,33 @@ """Batch processor filter service module""" + import boto3 import json from batch_audit_repository import BatchAuditRepository + from batch_file_created_event import BatchFileCreatedEvent from batch_file_repository import BatchFileRepository + from constants import REGION_NAME, FileStatus, QUEUE_URL, FileNotProcessedReason from exceptions import EventAlreadyProcessingForSupplierAndVaccTypeError from logger import logger from send_log_to_firehose import send_log_to_firehose +BATCH_AUDIT_REPOSITORY = BatchAuditRepository() +BATCH_FILE_REPOSITORY = BatchFileRepository() + class BatchProcessorFilterService: """Batch processor filter service class. Provides the business logic for the Lambda function""" + def __init__( self, - audit_repo: BatchAuditRepository = BatchAuditRepository(), - batch_file_repo: BatchFileRepository = BatchFileRepository() + audit_repo: BatchAuditRepository = BATCH_AUDIT_REPOSITORY, + batch_file_repo: BatchFileRepository = BATCH_FILE_REPOSITORY, ): self._batch_audit_repository = audit_repo self._batch_file_repo = batch_file_repo - self._queue_client = boto3.client('sqs', region_name=REGION_NAME) + self._queue_client = boto3.client("sqs", region_name=REGION_NAME) def _is_duplicate_file(self, file_key: str) -> bool: """Checks if a file with the same name has already been processed or marked for processing""" @@ -32,33 +39,38 @@ def apply_filter(self, batch_file_created_event: BatchFileCreatedEvent) -> None: supplier = batch_file_created_event["supplier"] vaccine_type = batch_file_created_event["vaccine_type"] - logger.info("Received batch file event for filename: %s with message id: %s", filename, message_id) + logger.info( + "Received batch file event for filename: %s with message id: %s", + filename, + message_id, + ) if self._is_duplicate_file(filename): # Mark as processed and return without error so next event will be picked up from queue logger.error("A duplicate file has already been processed. Filename: %s", filename) self._batch_audit_repository.update_status( message_id, - f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.DUPLICATE}" + f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.DUPLICATE}", ) self._batch_file_repo.upload_failure_ack(batch_file_created_event) self._batch_file_repo.move_source_file_to_archive(filename) return - if self._batch_audit_repository.is_event_processing_or_failed_for_supplier_and_vacc_type( - supplier, - vaccine_type - ): + if self._batch_audit_repository.is_event_processing_or_failed_for_supplier_and_vacc_type(supplier, vaccine_type): # Raise error so event is returned to queue and retried again later - logger.info("Batch event already processing for supplier and vacc type. Filename: %s", filename) - raise EventAlreadyProcessingForSupplierAndVaccTypeError(f"Batch event already processing for supplier: " - f"{supplier} and vacc type: {vaccine_type}") + logger.info( + "Batch event already processing for supplier and vacc type. Filename: %s", + filename, + ) + raise EventAlreadyProcessingForSupplierAndVaccTypeError( + f"Batch event already processing for supplier: {supplier} and vacc type: {vaccine_type}" + ) self._batch_audit_repository.update_status(message_id, FileStatus.PROCESSING) self._queue_client.send_message( QueueUrl=QUEUE_URL, MessageBody=json.dumps(batch_file_created_event), - MessageGroupId=f"{supplier}_{vaccine_type}" + MessageGroupId=f"{supplier}_{vaccine_type}", ) successful_log_message = f"File forwarded for processing by ECS. Filename: {filename}" diff --git a/batch_processor_filter/src/constants.py b/batch_processor_filter/src/constants.py index c00a755c2..33c5a1dba 100644 --- a/batch_processor_filter/src/constants.py +++ b/batch_processor_filter/src/constants.py @@ -24,6 +24,7 @@ class FileStatus(StrEnum): class FileNotProcessedReason(StrEnum): """Reasons why a file was not processed""" + DUPLICATE = "Duplicate" diff --git a/batch_processor_filter/src/exception_decorator.py b/batch_processor_filter/src/exception_decorator.py index b9d69d56b..f7bef1403 100644 --- a/batch_processor_filter/src/exception_decorator.py +++ b/batch_processor_filter/src/exception_decorator.py @@ -1,4 +1,5 @@ """Module for the batch processor filter Lambda exception wrapper""" + from functools import wraps from typing import Callable @@ -18,7 +19,10 @@ def exception_wrapper(*args, **kwargs): # Re-raise so event will be returned to SQS and retried for this expected error raise exc except Exception as exc: # pylint:disable = broad-exception-caught - logger.error("An unhandled exception occurred in the batch processor filter Lambda", exc_info=exc) + logger.error( + "An unhandled exception occurred in the batch processor filter Lambda", + exc_info=exc, + ) raise exc return exception_wrapper diff --git a/batch_processor_filter/src/exceptions.py b/batch_processor_filter/src/exceptions.py index 06ebc87e3..65a3f99a2 100644 --- a/batch_processor_filter/src/exceptions.py +++ b/batch_processor_filter/src/exceptions.py @@ -3,9 +3,11 @@ class InvalidBatchSizeError(Exception): """Raised when the SQS event batch size is anything other than 1""" + pass class EventAlreadyProcessingForSupplierAndVaccTypeError(Exception): """Raised when there is already a batch processing event in flight for the same supplier and vaccine type""" + pass diff --git a/batch_processor_filter/src/logger.py b/batch_processor_filter/src/logger.py index 77e8d26af..c6e5a24c4 100644 --- a/batch_processor_filter/src/logger.py +++ b/batch_processor_filter/src/logger.py @@ -1,4 +1,5 @@ """Module for the batch processor filter Lambda logger""" + import logging diff --git a/batch_processor_filter/tests/test_lambda_handler.py b/batch_processor_filter/tests/test_lambda_handler.py index 5d4d0a258..16d0c758d 100644 --- a/batch_processor_filter/tests/test_lambda_handler.py +++ b/batch_processor_filter/tests/test_lambda_handler.py @@ -10,13 +10,27 @@ from moto import mock_aws from batch_file_created_event import BatchFileCreatedEvent -from exceptions import InvalidBatchSizeError, EventAlreadyProcessingForSupplierAndVaccTypeError -from testing_utils import MOCK_ENVIRONMENT_DICT, make_sqs_record, add_entry_to_mock_table, get_audit_entry_status_by_id +from exceptions import ( + InvalidBatchSizeError, + EventAlreadyProcessingForSupplierAndVaccTypeError, +) +from testing_utils import ( + MOCK_ENVIRONMENT_DICT, + make_sqs_record, + add_entry_to_mock_table, + get_audit_entry_status_by_id, +) with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): from lambda_handler import lambda_handler - from constants import AUDIT_TABLE_NAME, REGION_NAME, AuditTableKeys, AUDIT_TABLE_FILENAME_GSI, \ - AUDIT_TABLE_QUEUE_NAME_GSI, FileStatus + from constants import ( + AUDIT_TABLE_NAME, + REGION_NAME, + AuditTableKeys, + AUDIT_TABLE_FILENAME_GSI, + AUDIT_TABLE_QUEUE_NAME_GSI, + FileStatus, + ) sqs_client = boto3.client("sqs", region_name=REGION_NAME) dynamodb_client = boto3.client("dynamodb", region_name=REGION_NAME) @@ -31,7 +45,7 @@ class TestLambdaHandler(TestCase): supplier="TESTSUPPLIER", permission=["some-permissions"], filename="Menacwy_Vaccinations_v5_TEST_20250820T10210000.csv", - created_at_formatted_string="20250826T14372600" + created_at_formatted_string="20250826T14372600", ) mock_queue_url = MOCK_ENVIRONMENT_DICT.get("QUEUE_URL") mock_source_bucket = MOCK_ENVIRONMENT_DICT.get("SOURCE_BUCKET_NAME") @@ -53,7 +67,10 @@ def setUp(self): "IndexName": AUDIT_TABLE_FILENAME_GSI, "KeySchema": [{"AttributeName": AuditTableKeys.FILENAME, "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, { "IndexName": AUDIT_TABLE_QUEUE_NAME_GSI, @@ -62,18 +79,22 @@ def setUp(self): {"AttributeName": AuditTableKeys.STATUS, "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, ], ) - sqs_client.create_queue(QueueName="imms-batch-metadata-queue.fifo", Attributes={ - "FifoQueue": "true", - "ContentBasedDeduplication": "true" - }) + sqs_client.create_queue( + QueueName="imms-batch-metadata-queue.fifo", + Attributes={"FifoQueue": "true", "ContentBasedDeduplication": "true"}, + ) for bucket_name in [self.mock_source_bucket, self.mock_ack_bucket]: s3_client.create_bucket( - Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) self.logger_patcher = patch("batch_processor_filter_service.logger") @@ -100,8 +121,10 @@ def _assert_source_file_moved(self, filename: str): with self.assertRaises(botocore.exceptions.ClientError) as exc: s3_client.get_object(Bucket=self.mock_source_bucket, Key=filename) - self.assertEqual(str(exc.exception), "An error occurred (NoSuchKey) when calling the GetObject " - "operation: The specified key does not exist.") + self.assertEqual( + str(exc.exception), + "An error occurred (NoSuchKey) when calling the GetObject " "operation: The specified key does not exist.", + ) archived_object = s3_client.get_object(Bucket=self.mock_source_bucket, Key=f"archive/{filename}") self.assertIsNotNone(archived_object) @@ -118,31 +141,37 @@ def test_lambda_handler_raises_error_when_empty_batch_received(self): def test_lambda_handler_raises_error_when_more_than_one_record_in_batch(self): with self.assertRaises(InvalidBatchSizeError) as exc: - lambda_handler({"Records": [ - make_sqs_record(self.default_batch_file_event), - make_sqs_record(self.default_batch_file_event), - ]}, Mock()) + lambda_handler( + { + "Records": [ + make_sqs_record(self.default_batch_file_event), + make_sqs_record(self.default_batch_file_event), + ] + }, + Mock(), + ) self.assertEqual(str(exc.exception), "Received 2 records, expected 1") def test_lambda_handler_decorator_logs_unhandled_exceptions(self): """The exception decorator should log the error when an unhandled exception occurs""" with self.assertRaises(JSONDecodeError): - lambda_handler({"Records": [ - { - "body": "{'malformed}" - } - ]}, Mock()) + lambda_handler({"Records": [{"body": "{'malformed}"}]}, Mock()) self.mock_exception_decorator_logger.error.assert_called_once_with( "An unhandled exception occurred in the batch processor filter Lambda", - exc_info=ANY + exc_info=ANY, ) def test_lambda_handler_handles_duplicate_file_scenario(self): """Should update the audit table status to duplicate for the incoming record""" # Add the duplicate entry that has already been processed - add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, self.default_batch_file_event, FileStatus.PROCESSED) + add_entry_to_mock_table( + dynamodb_client, + AUDIT_TABLE_NAME, + self.default_batch_file_event, + FileStatus.PROCESSED, + ) duplicate_file_event = copy.deepcopy(self.default_batch_file_event) duplicate_file_event["message_id"] = "fc9008b7-3865-4dcf-88b8-fc4abafff5f8" test_file_name = duplicate_file_event["filename"] @@ -164,22 +193,34 @@ def test_lambda_handler_handles_duplicate_file_scenario(self): self._assert_ack_file_created("Menacwy_Vaccinations_v5_TEST_20250820T10210000_InfAck_20250826T14372600.csv") self.mock_logger.error.assert_called_once_with( - "A duplicate file has already been processed. Filename: %s", - test_file_name + "A duplicate file has already been processed. Filename: %s", test_file_name ) - def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_and_vacc_type(self): + def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_and_vacc_type( + self, + ): """Should raise exception so that the event is returned to the originating queue to be retried later""" test_cases = { - ("Event is already being processed for supplier + vacc type queue", FileStatus.PROCESSING), - ("There is a failed event to be checked in supplier + vacc type queue", FileStatus.FAILED) + ( + "Event is already being processed for supplier + vacc type queue", + FileStatus.PROCESSING, + ), + ( + "There is a failed event to be checked in supplier + vacc type queue", + FileStatus.FAILED, + ), } for msg, file_status in test_cases: self.mock_logger.reset_mock() with self.subTest(msg=msg): # Add an audit entry for a batch event that is already processing or failed - add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, self.default_batch_file_event, file_status) + add_entry_to_mock_table( + dynamodb_client, + AUDIT_TABLE_NAME, + self.default_batch_file_event, + file_status, + ) test_event: BatchFileCreatedEvent = BatchFileCreatedEvent( message_id="3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e", @@ -187,7 +228,7 @@ def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_ supplier="TESTSUPPLIER", # Same supplier permission=["some-permissions"], filename="Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", # Different timestamp - created_at_formatted_string="20250826T15003000" + created_at_formatted_string="20250826T15003000", ) # Add the audit record for the incoming event add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, test_event, FileStatus.QUEUED) @@ -197,7 +238,7 @@ def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_ self.assertEqual( str(exc.exception), - "Batch event already processing for supplier: TESTSUPPLIER and vacc type: MENACWY" + "Batch event already processing for supplier: TESTSUPPLIER and vacc type: MENACWY", ) status = get_audit_entry_status_by_id(dynamodb_client, AUDIT_TABLE_NAME, test_event["message_id"]) @@ -206,52 +247,74 @@ def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_ sqs_messages = sqs_client.receive_message(QueueUrl=self.mock_queue_url) self.assertEqual(sqs_messages.get("Messages", []), []) - self.mock_logger.info.assert_has_calls([ - call( - "Received batch file event for filename: %s with message id: %s", - "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", - "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e" - ), - call( - "Batch event already processing for supplier and vacc type. Filename: %s", - "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv" - ) - ]) + self.mock_logger.info.assert_has_calls( + [ + call( + "Received batch file event for filename: %s with message id: %s", + "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", + "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e", + ), + call( + "Batch event already processing for supplier and vacc type. Filename: %s", + "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", + ), + ] + ) def test_lambda_handler_processes_event_successfully(self): """Should update the audit entry status to Processing and forward to SQS""" - add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, self.default_batch_file_event, FileStatus.QUEUED) + add_entry_to_mock_table( + dynamodb_client, + AUDIT_TABLE_NAME, + self.default_batch_file_event, + FileStatus.QUEUED, + ) lambda_handler({"Records": [make_sqs_record(self.default_batch_file_event)]}, Mock()) - status = get_audit_entry_status_by_id(dynamodb_client, AUDIT_TABLE_NAME, - self.default_batch_file_event["message_id"]) + status = get_audit_entry_status_by_id( + dynamodb_client, + AUDIT_TABLE_NAME, + self.default_batch_file_event["message_id"], + ) self.assertEqual(status, "Processing") sqs_messages = sqs_client.receive_message(QueueUrl=self.mock_queue_url) self.assertEqual(len(sqs_messages.get("Messages", [])), 1) - self.assertDictEqual(json.loads(sqs_messages["Messages"][0]["Body"]), dict(self.default_batch_file_event)) - - expected_success_log_message = (f"File forwarded for processing by ECS. Filename: " - f"{self.default_batch_file_event['filename']}") - self.mock_logger.info.assert_has_calls([ - call( - "Received batch file event for filename: %s with message id: %s", - "Menacwy_Vaccinations_v5_TEST_20250820T10210000.csv", - "df0b745c-b8cb-492c-ba84-8ea28d9f51d5" - ), - call( - expected_success_log_message - ) - ]) + self.assertDictEqual( + json.loads(sqs_messages["Messages"][0]["Body"]), + dict(self.default_batch_file_event), + ) + + expected_success_log_message = ( + f"File forwarded for processing by ECS. Filename: " f"{self.default_batch_file_event['filename']}" + ) + self.mock_logger.info.assert_has_calls( + [ + call( + "Received batch file event for filename: %s with message id: %s", + "Menacwy_Vaccinations_v5_TEST_20250820T10210000.csv", + "df0b745c-b8cb-492c-ba84-8ea28d9f51d5", + ), + call(expected_success_log_message), + ] + ) self.mock_firehose_send_log.assert_called_once_with( {**self.default_batch_file_event, "message": expected_success_log_message} ) - def test_lambda_handler_processes_event_successfully_when_event_for_same_supplier_and_vacc_already_processed(self): + def test_lambda_handler_processes_event_successfully_when_event_for_same_supplier_and_vacc_already_processed( + self, + ): """Should update the audit entry status to Processing and forward to SQS when there is already a file for - the same supplier and vaccine type in the audit table but it is no longer in Processing state""" - add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, self.default_batch_file_event, FileStatus.PROCESSED) + the same supplier and vaccine type in the audit table but it is no longer in Processing state + """ + add_entry_to_mock_table( + dynamodb_client, + AUDIT_TABLE_NAME, + self.default_batch_file_event, + FileStatus.PROCESSED, + ) test_event: BatchFileCreatedEvent = BatchFileCreatedEvent( message_id="3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e", @@ -259,7 +322,7 @@ def test_lambda_handler_processes_event_successfully_when_event_for_same_supplie supplier="TESTSUPPLIER", # Same supplier permission=["some-permissions"], filename="Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", # Different timestamp - created_at_formatted_string="20250826T15003000" + created_at_formatted_string="20250826T15003000", ) add_entry_to_mock_table(dynamodb_client, AUDIT_TABLE_NAME, test_event, FileStatus.QUEUED) @@ -273,16 +336,14 @@ def test_lambda_handler_processes_event_successfully_when_event_for_same_supplie self.assertDictEqual(json.loads(sqs_messages["Messages"][0]["Body"]), dict(test_event)) expected_success_log_message = f"File forwarded for processing by ECS. Filename: {test_event['filename']}" - self.mock_logger.info.assert_has_calls([ - call( - "Received batch file event for filename: %s with message id: %s", - "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", - "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e" - ), - call( - expected_success_log_message - ) - ]) - self.mock_firehose_send_log.assert_called_once_with( - {**test_event, "message": expected_success_log_message} + self.mock_logger.info.assert_has_calls( + [ + call( + "Received batch file event for filename: %s with message id: %s", + "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv", + "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e", + ), + call(expected_success_log_message), + ] ) + self.mock_firehose_send_log.assert_called_once_with({**test_event, "message": expected_success_log_message}) diff --git a/batch_processor_filter/tests/testing_utils.py b/batch_processor_filter/tests/testing_utils.py index 0b8a0ab85..604a9c973 100644 --- a/batch_processor_filter/tests/testing_utils.py +++ b/batch_processor_filter/tests/testing_utils.py @@ -10,7 +10,7 @@ "FILE_NAME_GSI": "filename_index", "QUEUE_NAME_GSI": "queue_name_index", "SOURCE_BUCKET_NAME": "immunisation-batch-internal-dev-data-sources", - "ACK_BUCKET_NAME": "immunisation-batch-internal-dev-data-destinations" + "ACK_BUCKET_NAME": "immunisation-batch-internal-dev-data-destinations", } @@ -19,25 +19,29 @@ def make_sqs_record(batch_file_created_event: BatchFileCreatedEvent) -> SQSMessa return { "messageId": "1234", "eventSource": "aws:sqs", - "body": json.dumps(batch_file_created_event) + "body": json.dumps(batch_file_created_event), } -def add_entry_to_mock_table(dynamodb_client, table_name: str, batch_file_created_event: BatchFileCreatedEvent, - status: str) -> None: +def add_entry_to_mock_table( + dynamodb_client, + table_name: str, + batch_file_created_event: BatchFileCreatedEvent, + status: str, +) -> None: """Add an entry to the audit table""" audit_table_entry = { "message_id": {"S": batch_file_created_event.get("message_id")}, "queue_name": {"S": f'{batch_file_created_event["supplier"]}_{batch_file_created_event["vaccine_type"]}'}, "filename": {"S": batch_file_created_event.get("filename")}, - "status": {"S": status} + "status": {"S": status}, } dynamodb_client.put_item(TableName=table_name, Item=audit_table_entry) def get_audit_entry_status_by_id(dynamodb_client, table_name: str, audit_entry_id: str) -> str | None: - audit_entry = dynamodb_client.get_item( - TableName=table_name, Key={"message_id": {"S": audit_entry_id}} - ).get("Item", {}) + audit_entry = dynamodb_client.get_item(TableName=table_name, Key={"message_id": {"S": audit_entry_id}}).get( + "Item", {} + ) return audit_entry.get("status", {}).get("S") diff --git a/config/common/disease_mapping.json b/config/common/disease_mapping.json index a04f77fd2..0672b3abd 100644 --- a/config/common/disease_mapping.json +++ b/config/common/disease_mapping.json @@ -1,81 +1,81 @@ -[ - { - "vacc_type": "3IN1", - "diseases": [ - { - "code": "398102009", - "term": "Acute poliomyelitis" - }, - { - "code": "397430003", - "term": "Diphtheria caused by Corynebacterium diphtheriae" - }, - { - "code": "76902006", - "term": "Tetanus (disorder)" - } - ] - }, - { - "vacc_type": "COVID19", - "diseases": [ - { - "code": "840539006", - "term": "Disease caused by severe acute respiratory syndrome coronavirus 2" - } - ] - }, - { - "vacc_type": "FLU", - "diseases": [ - { - "code": "6142004", - "term": "Influenza caused by seasonal influenza virus (disorder)" - } - ] - }, - { - "vacc_type": "HPV", - "diseases": [ - { - "code": "240532009", - "term": "Human papillomavirus infection" - } - ] - }, - { - "vacc_type": "MENACWY", - "diseases": [ - { - "code": "23511006", - "term": "Meningococcal infectious disease" - } - ] - }, - { - "vacc_type": "MMR", - "diseases": [ - { - "code": "14189004", - "term": "Measles (disorder)" - }, - { - "code": "36989005", - "term": "Mumps (disorder)" - }, - { - "code": "36653000", - "term": "Rubella (disorder)" - } - ] - }, - { - "vacc_type": "RSV", - "diseases": [ - { - "code": "55735004", - "term": "Respiratory syncytial virus infection (disorder)" - } - ] - } -] +[ + { + "vacc_type": "3IN1", + "diseases": [ + { + "code": "398102009", + "term": "Acute poliomyelitis" + }, + { + "code": "397430003", + "term": "Diphtheria caused by Corynebacterium diphtheriae" + }, + { + "code": "76902006", + "term": "Tetanus (disorder)" + } + ] + }, + { + "vacc_type": "COVID19", + "diseases": [ + { + "code": "840539006", + "term": "Disease caused by severe acute respiratory syndrome coronavirus 2" + } + ] + }, + { + "vacc_type": "FLU", + "diseases": [ + { + "code": "6142004", + "term": "Influenza caused by seasonal influenza virus (disorder)" + } + ] + }, + { + "vacc_type": "HPV", + "diseases": [ + { + "code": "240532009", + "term": "Human papillomavirus infection" + } + ] + }, + { + "vacc_type": "MENACWY", + "diseases": [ + { + "code": "23511006", + "term": "Meningococcal infectious disease" + } + ] + }, + { + "vacc_type": "MMR", + "diseases": [ + { + "code": "14189004", + "term": "Measles (disorder)" + }, + { + "code": "36989005", + "term": "Mumps (disorder)" + }, + { + "code": "36653000", + "term": "Rubella (disorder)" + } + ] + }, + { + "vacc_type": "RSV", + "diseases": [ + { + "code": "55735004", + "term": "Respiratory syncytial virus infection (disorder)" + } + ] + } +] diff --git a/config/dev/permissions_config.json b/config/dev/permissions_config.json index f8bd65e96..4432ee100 100644 --- a/config/dev/permissions_config.json +++ b/config/dev/permissions_config.json @@ -87,10 +87,7 @@ }, { "supplier": "OXDH", - "permissions": [ - "COVID19.CRUDS", - "FLU.CRUDS" - ] + "permissions": ["COVID19.CRUDS", "FLU.CRUDS"] }, { "supplier": "PINNACLE", diff --git a/config/preprod/permissions_config.json b/config/preprod/permissions_config.json index cba91db26..12a2fc70f 100644 --- a/config/preprod/permissions_config.json +++ b/config/preprod/permissions_config.json @@ -77,10 +77,7 @@ }, { "supplier": "OXDH", - "permissions": [ - "COVID19.CRUDS", - "FLU.CRUDS" - ] + "permissions": ["COVID19.CRUDS", "FLU.CRUDS"] }, { "supplier": "Test_App", diff --git a/config/prod/permissions_config.json b/config/prod/permissions_config.json index 013e9c20e..239faca20 100644 --- a/config/prod/permissions_config.json +++ b/config/prod/permissions_config.json @@ -1,38 +1,22 @@ [ { "supplier": "DPSFULL", - "permissions": [ - "FLU.CRUDS", - "HPV.CRUDS", - "MMR.CRUDS", - "RSV.CRUDS" - ], + "permissions": ["FLU.CRUDS", "HPV.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], "ods_codes": ["DPSFULL"] }, { "supplier": "DPSREDUCED", - "permissions": [ - "FLU.CRUDS", - "HPV.CRUDS", - "MMR.CRUDS", - "RSV.CRUDS" - ], + "permissions": ["FLU.CRUDS", "HPV.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], "ods_codes": ["DPSREDUCED"] }, { "supplier": "RAVS", - "permissions": [ - "MMR.CRUDS", - "RSV.RS" - ], + "permissions": ["MMR.CRUDS", "RSV.RS"], "ods_codes": ["X26", "X8E5B"] }, { "supplier": "MAVIS", - "permissions": [ - "FLU.CRUDS", - "HPV.CUD" - ], + "permissions": ["FLU.CRUDS", "HPV.CUD"], "ods_codes": ["V0V8L"] }, { diff --git a/delta_backend/README.md b/delta_backend/README.md index 85d35a94a..a2ae4da48 100644 --- a/delta_backend/README.md +++ b/delta_backend/README.md @@ -6,24 +6,26 @@ This project is designed to convert FHIR-compliant JSON data (e.g., Immunization ## 📁 File Structure Overview -| File Name | What It Does | -|------------------------|---------------| -| **`converter.py`** | 🧠 The main brain — applies the schema, runs conversions, handles errors. | -| **`conversion_layout.py`** | A plain Python list that defines which fields you want, and how they should be formatted (e.g. date format, renaming rules). | -| **`delta.py`** | Holds the function called by AWS Lambda | -| **`extractor.py`** | Tailored functionality to extract target fields from immunization record received by the delta handler. | -| **`exception_messages.py`** | Holds reusable error messages and codes for clean debugging and validation feedback. | -| **`log_firehose.py`** | Firehose logging functionality. | -| **`utils.py`** | Holds utility functions. | ---- +| File Name | What It Does | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| **`converter.py`** | 🧠 The main brain — applies the schema, runs conversions, handles errors. | +| **`conversion_layout.py`** | A plain Python list that defines which fields you want, and how they should be formatted (e.g. date format, renaming rules). | +| **`delta.py`** | Holds the function called by AWS Lambda | +| **`extractor.py`** | Tailored functionality to extract target fields from immunization record received by the delta handler. | +| **`exception_messages.py`** | Holds reusable error messages and codes for clean debugging and validation feedback. | +| **`log_firehose.py`** | Firehose logging functionality. | +| **`utils.py`** | Holds utility functions. | +--- ## Setting up the delta lambda locally + Note: Paths are relative to this directory, `delta_backend`. 1. Follow the instructions in the root level README.md to setup the [dependencies](../README.md#environment-setup) and create a [virtual environment](../README.md#) for this folder. 2. Replace the `.env` file in the `delta_backend` folder. Note the variables might change in the future. These environment variables will be loaded automatically when using `direnv`. + ``` AWS_PROFILE= DYNAMODB_TABLE_NAME= @@ -71,7 +73,9 @@ python check_conversion.py ``` ### Output Location + When the script runs, it will automatically: + - Save a **flat JSON file** as `output.json` - Save a **CSV file** as `output.csv` @@ -83,6 +87,8 @@ These will be located one level up from the `tests/` folder: ``` ### Visualization + You can now: + - Open `output.csv` in Excel or Google Sheets to view cleanly structured records - Inspect `output.json` to validate the flat key-value output programmatically diff --git a/delta_backend/src/common/mappings.py b/delta_backend/src/common/mappings.py index 3541bbb23..f6b569b51 100644 --- a/delta_backend/src/common/mappings.py +++ b/delta_backend/src/common/mappings.py @@ -1,8 +1,8 @@ from enum import Enum -""" - Enums for event names, operations, and action flags. - +""" + Enums for event names, operations, and action flags. + # case eventName operation actionFlag ----------------- --------- --------- ---------- create INSERT CREATE NEW @@ -11,29 +11,34 @@ physical delete REMOVE REMOVE N/A """ -class EventName(): + +class EventName: CREATE = "INSERT" UPDATE = "MODIFY" DELETE_LOGICAL = "MODIFY" DELETE_PHYSICAL = "REMOVE" -class Operation(): + +class Operation: CREATE = "CREATE" UPDATE = "UPDATE" DELETE_LOGICAL = "DELETE" DELETE_PHYSICAL = "REMOVE" -class ActionFlag(): + +class ActionFlag: CREATE = "NEW" UPDATE = "UPDATE" DELETE_LOGICAL = "DELETE" - + + class Gender(Enum): MALE = "1" FEMALE = "2" OTHER = "9" UNKNOWN = "0" - + + class ConversionFieldName(str, Enum): NHS_NUMBER = "NHS_NUMBER" PERSON_FORENAME = "PERSON_FORENAME" diff --git a/delta_backend/src/conversion_layout.py b/delta_backend/src/conversion_layout.py index a53a292df..3d7011bb9 100644 --- a/delta_backend/src/conversion_layout.py +++ b/delta_backend/src/conversion_layout.py @@ -1,13 +1,15 @@ -from extractor import Extractor from common.mappings import ConversionFieldName +from extractor import Extractor + class ConversionField: def __init__(self, field_name_flat: str, expression_rule): self.field_name_flat = field_name_flat self.expression_rule = expression_rule + class ConversionLayout: - def __init__(self, extractor: Extractor): + def __init__(self, extractor: Extractor): self.extractor = extractor self.conversion_layout = [ ConversionField(ConversionFieldName.NHS_NUMBER, extractor.extract_nhs_number), @@ -18,32 +20,71 @@ def __init__(self, extractor: Extractor): ConversionField(ConversionFieldName.PERSON_POSTCODE, extractor.extract_valid_address), ConversionField(ConversionFieldName.DATE_AND_TIME, extractor.extract_date_time), ConversionField(ConversionFieldName.SITE_CODE, extractor.extract_site_code), - ConversionField(ConversionFieldName.SITE_CODE_TYPE_URI, extractor.extract_site_code_type_uri), + ConversionField( + ConversionFieldName.SITE_CODE_TYPE_URI, + extractor.extract_site_code_type_uri, + ), ConversionField(ConversionFieldName.UNIQUE_ID, extractor.extract_unique_id), ConversionField(ConversionFieldName.UNIQUE_ID_URI, extractor.extract_unique_id_uri), ConversionField(ConversionFieldName.ACTION_FLAG, ""), - ConversionField(ConversionFieldName.PERFORMING_PROFESSIONAL_FORENAME, extractor.extract_practitioner_forename), - ConversionField(ConversionFieldName.PERFORMING_PROFESSIONAL_SURNAME, extractor.extract_practitioner_surname), + ConversionField( + ConversionFieldName.PERFORMING_PROFESSIONAL_FORENAME, + extractor.extract_practitioner_forename, + ), + ConversionField( + ConversionFieldName.PERFORMING_PROFESSIONAL_SURNAME, + extractor.extract_practitioner_surname, + ), ConversionField(ConversionFieldName.RECORDED_DATE, extractor.extract_recorded_date), ConversionField(ConversionFieldName.PRIMARY_SOURCE, extractor.extract_primary_source), - ConversionField(ConversionFieldName.VACCINATION_PROCEDURE_CODE, extractor.extract_vaccination_procedure_code), - ConversionField(ConversionFieldName.VACCINATION_PROCEDURE_TERM, extractor.extract_vaccination_procedure_term), + ConversionField( + ConversionFieldName.VACCINATION_PROCEDURE_CODE, + extractor.extract_vaccination_procedure_code, + ), + ConversionField( + ConversionFieldName.VACCINATION_PROCEDURE_TERM, + extractor.extract_vaccination_procedure_term, + ), ConversionField(ConversionFieldName.DOSE_SEQUENCE, extractor.extract_dose_sequence), - ConversionField(ConversionFieldName.VACCINE_PRODUCT_CODE, extractor.extract_vaccine_product_code), - ConversionField(ConversionFieldName.VACCINE_PRODUCT_TERM, extractor.extract_vaccine_product_term), - ConversionField(ConversionFieldName.VACCINE_MANUFACTURER, extractor.extract_vaccine_manufacturer), + ConversionField( + ConversionFieldName.VACCINE_PRODUCT_CODE, + extractor.extract_vaccine_product_code, + ), + ConversionField( + ConversionFieldName.VACCINE_PRODUCT_TERM, + extractor.extract_vaccine_product_term, + ), + ConversionField( + ConversionFieldName.VACCINE_MANUFACTURER, + extractor.extract_vaccine_manufacturer, + ), ConversionField(ConversionFieldName.BATCH_NUMBER, extractor.extract_batch_number), ConversionField(ConversionFieldName.EXPIRY_DATE, extractor.extract_expiry_date), - ConversionField(ConversionFieldName.SITE_OF_VACCINATION_CODE, extractor.extract_site_of_vaccination_code), - ConversionField(ConversionFieldName.SITE_OF_VACCINATION_TERM, extractor.extract_site_of_vaccination_term), - ConversionField(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, extractor.extract_route_of_vaccination_code), - ConversionField(ConversionFieldName.ROUTE_OF_VACCINATION_TERM, extractor.extract_route_of_vaccination_term), + ConversionField( + ConversionFieldName.SITE_OF_VACCINATION_CODE, + extractor.extract_site_of_vaccination_code, + ), + ConversionField( + ConversionFieldName.SITE_OF_VACCINATION_TERM, + extractor.extract_site_of_vaccination_term, + ), + ConversionField( + ConversionFieldName.ROUTE_OF_VACCINATION_CODE, + extractor.extract_route_of_vaccination_code, + ), + ConversionField( + ConversionFieldName.ROUTE_OF_VACCINATION_TERM, + extractor.extract_route_of_vaccination_term, + ), ConversionField(ConversionFieldName.DOSE_AMOUNT, extractor.extract_dose_amount), ConversionField(ConversionFieldName.DOSE_UNIT_CODE, extractor.extract_dose_unit_code), ConversionField(ConversionFieldName.DOSE_UNIT_TERM, extractor.extract_dose_unit_term), ConversionField(ConversionFieldName.INDICATION_CODE, extractor.extract_indication_code), ConversionField(ConversionFieldName.LOCATION_CODE, extractor.extract_location_code), - ConversionField(ConversionFieldName.LOCATION_CODE_TYPE_URI, extractor.extract_location_code_type_uri), + ConversionField( + ConversionFieldName.LOCATION_CODE_TYPE_URI, + extractor.extract_location_code_type_uri, + ), ] def get_conversion_layout(self): diff --git a/delta_backend/src/converter.py b/delta_backend/src/converter.py index 270104154..80c0510e3 100644 --- a/delta_backend/src/converter.py +++ b/delta_backend/src/converter.py @@ -1,23 +1,24 @@ # Main validation engine import exception_messages +from common.mappings import ActionFlag from conversion_layout import ConversionLayout, ConversionField from extractor import Extractor -from common.mappings import ActionFlag + class Converter: - def __init__(self, fhir_data, action_flag = ActionFlag.UPDATE, report_unexpected_exception=True): + def __init__(self, fhir_data, action_flag=ActionFlag.UPDATE, report_unexpected_exception=True): self.converted = {} self.error_records = [] self.action_flag = action_flag self.report_unexpected_exception = report_unexpected_exception - + try: if not fhir_data: raise ValueError("FHIR data is required for initialization.") - + self.extractor = Extractor(fhir_data, self.report_unexpected_exception) - self.conversion_layout = ConversionLayout(self.extractor) + self.conversion_layout = ConversionLayout(self.extractor) except Exception as e: if report_unexpected_exception: self._log_error(f"Initialization failed: [{e.__class__.__name__}] {e}") @@ -25,10 +26,10 @@ def __init__(self, fhir_data, action_flag = ActionFlag.UPDATE, report_unexpected def run_conversion(self): conversions = self.conversion_layout.get_conversion_layout() - + for conversion in conversions: self._convert_data(conversion) - + self.error_records.extend(self.extractor.get_error_records()) # Add CONVERSION_ERRORS as the 35th field @@ -38,7 +39,7 @@ def run_conversion(self): def _convert_data(self, conversion: ConversionField): try: flat_field = conversion.field_name_flat - + if flat_field == "ACTION_FLAG": self.converted[flat_field] = self.action_flag else: @@ -47,17 +48,17 @@ def _convert_data(self, conversion: ConversionField): self.converted[flat_field] = converted except Exception as e: - self._log_error(f"Conversion error [{e.__class__.__name__}]: {e}", code=exception_messages.PARSING_ERROR) + self._log_error( + f"Conversion error [{e.__class__.__name__}]: {e}", + code=exception_messages.PARSING_ERROR, + ) self.converted[flat_field] = "" - def _log_error(self,e,code=exception_messages.UNEXPECTED_EXCEPTION): - error_obj = { - "code": code, - "message": str(e) - } - + def _log_error(self, e, code=exception_messages.UNEXPECTED_EXCEPTION): + error_obj = {"code": code, "message": str(e)} + if self.report_unexpected_exception: self.error_records.append(error_obj) - + def get_error_records(self): - return self.error_records \ No newline at end of file + return self.error_records diff --git a/delta_backend/src/delta.py b/delta_backend/src/delta.py index bca0fccf5..155772363 100644 --- a/delta_backend/src/delta.py +++ b/delta_backend/src/delta.py @@ -4,7 +4,6 @@ import os import time from datetime import datetime, timedelta, UTC -from unittest import case import boto3 from boto3.dynamodb.conditions import Attr @@ -25,6 +24,8 @@ firehose_logger = FirehoseLogger() delta_table = None + + def get_delta_table(): """ Initialize the DynamoDB table resource with exception handling. @@ -40,7 +41,10 @@ def get_delta_table(): delta_table = None return delta_table + sqs_client = None + + def get_sqs_client(): """ Initialize the SQS client with exception handling. @@ -55,6 +59,7 @@ def get_sqs_client(): sqs_client = None return sqs_client + def send_message(record, queue_url=failure_queue_url): # Create a message message_body = record @@ -65,19 +70,23 @@ def send_message(record, queue_url=failure_queue_url): except Exception: logger.exception("Error sending record to DLQ") + def get_vaccine_type(patient_sort_key: str) -> str: vaccine_type = patient_sort_key.split("#")[0] return str.strip(str.lower(vaccine_type)) + def get_imms_id(primary_key: str) -> str: return primary_key.split("#")[1] + def get_creation_and_expiry_times(creation_timestamp: float) -> (str, int): creation_datetime = datetime.fromtimestamp(creation_timestamp, UTC) expiry_datetime = creation_datetime + timedelta(days=int(delta_ttl_days)) expiry_timestamp = int(expiry_datetime.timestamp()) return creation_datetime.isoformat(), expiry_timestamp + def send_firehose(log_data): try: firehose_log = {"event": log_data} @@ -85,26 +94,50 @@ def send_firehose(log_data): except Exception: logger.exception("Error sending log to Firehose") + def handle_dynamodb_response(response, error_records): match response: case {"ResponseMetadata": {"HTTPStatusCode": 200}} if error_records: - logger.warning(f"Partial success: successfully synced into delta, but issues found within record: {json.dumps(error_records)}") - return True, {"statusCode": "207", "statusDesc": "Partial success: successfully synced into delta, but issues found within record", "diagnostics": error_records} + logger.warning( + "Partial success: successfully synced into delta, " + f"but issues found within record: {json.dumps(error_records)}" + ) + return True, { + "statusCode": "207", + "statusDesc": "Partial success: successfully synced into delta, but issues found within record", + "diagnostics": error_records, + } case {"ResponseMetadata": {"HTTPStatusCode": 200}}: logger.info("Successfully synched into delta") - return True, {"statusCode": "200", "statusDesc": "Successfully synched into delta"} + return True, { + "statusCode": "200", + "statusDesc": "Successfully synched into delta", + } case _: logger.error(f"Failure response from DynamoDB: {response}") - return False, {"statusCode": "500", "statusDesc": "Failure response from DynamoDB", "diagnostics": response} + return False, { + "statusCode": "500", + "statusDesc": "Failure response from DynamoDB", + "diagnostics": response, + } + def handle_exception_response(response): match response: case ClientError(response={"Error": {"Code": "ConditionalCheckFailedException"}}): logger.info("Skipped record already present in delta") - return True, {"statusCode": "200", "statusDesc": "Skipped record already present in delta"} + return True, { + "statusCode": "200", + "statusDesc": "Skipped record already present in delta", + } case _: logger.exception("Exception during processing") - return False, {"statusCode": "500", "statusDesc": "Exception", "diagnostics": response} + return False, { + "statusCode": "500", + "statusDesc": "Exception", + "diagnostics": response, + } + def process_remove(record): event_id = record["eventID"] @@ -137,11 +170,17 @@ def process_remove(record): operation_outcome.update(extra_log_fields) return success, operation_outcome + def process_skip(record): primary_key = record["dynamodb"]["NewImage"]["PK"]["S"] imms_id = get_imms_id(primary_key) logger.info("Record from DPS skipped") - return True, {"record": imms_id, "statusCode": "200", "statusDesc": "Record from DPS skipped"} + return True, { + "record": imms_id, + "statusCode": "200", + "statusDesc": "Record from DPS skipped", + } + def process_create_update_delete(record): event_id = record["eventID"] @@ -183,6 +222,7 @@ def process_create_update_delete(record): operation_outcome.update(extra_log_fields) return success, operation_outcome + def process_record(record): try: if record["eventName"] == EventName.DELETE_PHYSICAL: @@ -197,6 +237,7 @@ def process_record(record): logger.exception("Exception during processing") return False, {"statusCode": "500", "statusDesc": "Exception", "diagnostics": e} + def handler(event, _context): overall_success = True logger.info("Starting Delta Handler") @@ -207,18 +248,20 @@ def handler(event, _context): success, operation_outcome = process_record(record) overall_success = overall_success and success end = time.time() - send_firehose({ - "function_name": "delta_sync", - "operation_outcome": operation_outcome, - "date_time": datetime_str, - "time_taken": f"{round(end - start, 5)}s" - }) + send_firehose( + { + "function_name": "delta_sync", + "operation_outcome": operation_outcome, + "date_time": datetime_str, + "time_taken": f"{round(end - start, 5)}s", + } + ) except Exception: overall_success = False operation_outcome = { "statusCode": "500", "statusDesc": "Exception", - "diagnostics": "Delta Lambda failure: Incorrect invocation of Lambda" + "diagnostics": "Delta Lambda failure: Incorrect invocation of Lambda", } logger.exception(operation_outcome["diagnostics"]) send_message(event) # Send failed records to DLQ diff --git a/delta_backend/src/extractor.py b/delta_backend/src/extractor.py index 84135e455..02d6e5404 100644 --- a/delta_backend/src/extractor.py +++ b/delta_backend/src/extractor.py @@ -1,14 +1,18 @@ import decimal import json -import exception_messages from datetime import datetime, timedelta, timezone + +import exception_messages from common.mappings import Gender, ConversionFieldName + class Extractor: # This file holds the schema/base layout that maps FHIR fields to flat JSON fields # Each entry tells the converter how to extract and transform a specific value - EXTENSION_URL_VACCINATION_PRODEDURE = "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + EXTENSION_URL_VACCINATION_PRODEDURE = ( + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + ) EXTENSION_URL_SCT_DESC_DISPLAY = "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay" CODING_SYSTEM_URL_SNOMED = "http://snomed.info/sct" @@ -19,14 +23,21 @@ class Extractor: DATE_CONVERT_FORMAT = "%Y%m%d" DEFAULT_POSTCODE = "ZZ99 3CZ" - def __init__(self, fhir_json_data, report_unexpected_exception = True): - self.fhir_json_data = json.loads(fhir_json_data, parse_float=decimal.Decimal) if isinstance(fhir_json_data, str) else fhir_json_data + def __init__(self, fhir_json_data, report_unexpected_exception=True): + self.fhir_json_data = ( + json.loads(fhir_json_data, parse_float=decimal.Decimal) + if isinstance(fhir_json_data, str) + else fhir_json_data + ) self.report_unexpected_exception = report_unexpected_exception self.error_records = [] def _get_patient(self): contained = self.fhir_json_data.get("contained", []) - return next((c for c in contained if isinstance(c, dict) and c.get("resourceType") == "Patient"), "") + return next( + (c for c in contained if isinstance(c, dict) and c.get("resourceType") == "Patient"), + "", + ) def _get_valid_names(self, names, occurrence_time): @@ -40,8 +51,6 @@ def _get_valid_names(self, names, occurrence_time): return names[0] - - def _get_person_names(self): occurrence_time = self._get_occurrence_date_time() patient = self._get_patient() @@ -62,7 +71,10 @@ def _get_person_names(self): def _get_practitioner_names(self): contained = self.fhir_json_data.get("contained", []) occurrence_time = self._get_occurrence_date_time() - practitioner = next((c for c in contained if isinstance(c, dict) and c.get("resourceType") == "Practitioner"), None) + practitioner = next( + (c for c in contained if isinstance(c, dict) and c.get("resourceType") == "Practitioner"), + None, + ) if not practitioner or "name" not in practitioner: return "", "" @@ -150,8 +162,7 @@ def _get_site_information(self): ( p for p in valid_performers - if p.get("actor", {}).get("identifier", {}).get("system") - == self.ODS_ORG_CODE_SYSTEM_URL + if p.get("actor", {}).get("identifier", {}).get("system") == self.ODS_ORG_CODE_SYSTEM_URL ), next( (p for p in valid_performers if p.get("actor", {}).get("type") == "Organization"), @@ -167,16 +178,21 @@ def _get_site_information(self): def _log_error(self, field_name, field_value, e, code=exception_messages.RECORD_CHECK_FAILED): if self.report_unexpected_exception: if isinstance(e, Exception): - message = exception_messages.MESSAGES[exception_messages.UNEXPECTED_EXCEPTION] % (e.__class__.__name__, str(e)) + message = exception_messages.MESSAGES[exception_messages.UNEXPECTED_EXCEPTION] % ( + e.__class__.__name__, + str(e), + ) else: message = str(e) - self.error_records.append({ - "code": code, - "field": field_name, - "value": field_value, - "message": message - }) + self.error_records.append( + { + "code": code, + "field": field_name, + "value": field_value, + "message": message, + } + ) def _convert_date(self, field_name, date, format) -> str: """ @@ -260,23 +276,41 @@ def extract_valid_address(self): if len(addresses) == 1: return addresses[0].get("postalCode") or self.DEFAULT_POSTCODE - if not (valid_addresses := [ - addr for addr in addresses - if addr.get("postalCode") and self._is_current_period(addr, occurrence_time) - ]): + if not ( + valid_addresses := [ + addr for addr in addresses if addr.get("postalCode") and self._is_current_period(addr, occurrence_time) + ] + ): return self.DEFAULT_POSTCODE selected_address = ( - next((a for a in valid_addresses if self.normalize(a.get("use")) == "home" and self.normalize(a.get("type")) != "postal"), None) - or next((a for a in valid_addresses if self.normalize(a.get("use")) != "old" and self.normalize(a.get("type")) != "postal"), None) - or next((a for a in valid_addresses if self.normalize(a.get("use")) != "old"), None) + next( + ( + a + for a in valid_addresses + if self.normalize(a.get("use")) == "home" and self.normalize(a.get("type")) != "postal" + ), + None, + ) + or next( + ( + a + for a in valid_addresses + if self.normalize(a.get("use")) != "old" and self.normalize(a.get("type")) != "postal" + ), + None, + ) + or next( + (a for a in valid_addresses if self.normalize(a.get("use")) != "old"), + None, + ) or valid_addresses[0] ) return selected_address.get("postalCode") or self.DEFAULT_POSTCODE def extract_date_time(self) -> str: - date = self.fhir_json_data.get("occurrenceDateTime","") + date = self.fhir_json_data.get("occurrenceDateTime", "") if date: return self._convert_date_to_safe_format(ConversionFieldName.DATE_AND_TIME, date) return "" @@ -356,7 +390,7 @@ def extract_batch_number(self) -> str: return self.fhir_json_data.get("lotNumber", "") def extract_expiry_date(self) -> str: - date = self.fhir_json_data.get("expirationDate","") + date = self.fhir_json_data.get("expirationDate", "") return self._convert_date(ConversionFieldName.EXPIRY_DATE, date, self.DATE_CONVERT_FORMAT) def extract_site_of_vaccination_code(self) -> str: diff --git a/delta_backend/src/log_firehose.py b/delta_backend/src/log_firehose.py index b86e87e2f..c5c8134f4 100644 --- a/delta_backend/src/log_firehose.py +++ b/delta_backend/src/log_firehose.py @@ -1,19 +1,24 @@ -import boto3 -import logging import json +import logging import os + + +import boto3 from botocore.config import Config logging.basicConfig() logger = logging.getLogger() logger.setLevel("INFO") +STREAM_NAME = os.getenv("SPLUNK_FIREHOSE_NAME") +BOTO_CLIENT = boto3.client("firehose", config=Config(region_name="eu-west-2")) + class FirehoseLogger: def __init__( self, - stream_name: str = os.getenv("SPLUNK_FIREHOSE_NAME"), - boto_client=boto3.client("firehose", config=Config(region_name="eu-west-2")), + stream_name: str = STREAM_NAME, + boto_client=BOTO_CLIENT, ): self.firehose_client = boto_client self.delivery_stream_name = stream_name diff --git a/delta_backend/src/utils.py b/delta_backend/src/utils.py index 3a19ad760..f0c91ce46 100644 --- a/delta_backend/src/utils.py +++ b/delta_backend/src/utils.py @@ -1,14 +1,14 @@ - from stdnum.verhoeff import validate - + + def is_valid_simple_snomed(simple_snomed: str) -> bool: """ - This utility is designed for reuse and should be packaged as part of a + This utility is designed for reuse and should be packaged as part of a shared validation module or service. """ min_snomed_length = 6 max_snomed_length = 18 - try: + try: return ( simple_snomed is not None and simple_snomed.isdigit() @@ -16,6 +16,5 @@ def is_valid_simple_snomed(simple_snomed: str) -> bool: and validate(simple_snomed) and (simple_snomed[-3:-1] in ("00", "10")) ) - except: + except Exception: return False - \ No newline at end of file diff --git a/delta_backend/tests/check_conversion.py b/delta_backend/tests/check_conversion.py index 6590f96dc..ff4918a7e 100644 --- a/delta_backend/tests/check_conversion.py +++ b/delta_backend/tests/check_conversion.py @@ -1,13 +1,14 @@ -import json import csv +import json import os import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) from converter import Converter +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + # Sample FHIR Immunization resource (minimal test data) -fhir_sample = os.path.join(os.path.dirname(__file__),"sample_data", "fhir_sample.json") +fhir_sample = os.path.join(os.path.dirname(__file__), "sample_data", "fhir_sample.json") with open(fhir_sample, "r", encoding="utf-8") as f: diff --git a/delta_backend/tests/sample_data/TestLayout.json b/delta_backend/tests/sample_data/TestLayout.json index a95fd75e9..34cc3a5a9 100644 --- a/delta_backend/tests/sample_data/TestLayout.json +++ b/delta_backend/tests/sample_data/TestLayout.json @@ -1,27 +1,26 @@ -{ - "id": "7d78e9a6-d859-45d3-bb05-df9c405acbdb", - "schemaName": "JSON Base", - "version": 1.0, - "releaseDate": "2024-07-17T00:00:00.000Z", - "conversions": [ - { - "fieldNameFHIR": "performer.#:Organization.actor.identifier.value", - "fieldNameFlat": "SITE_CODE", - "expression": { - "expressionName": "Not Empty", - "expressionType": "NOTEMPTY", - "expressionRule": "" - } - }, - { - "fieldNameFHIR": "performer.#:Organization.actor.identifier.system", - "fieldNameFlat": "SITE_CODE_TYPE_URI", - "expression": { - "expressionName": "Defaults to", - "expressionType": "DEFAULT", - "expressionRule": "https://fhir.nhs.uk/Id/ods-organization-code" - } - } - ] - } - \ No newline at end of file +{ + "id": "7d78e9a6-d859-45d3-bb05-df9c405acbdb", + "schemaName": "JSON Base", + "version": 1.0, + "releaseDate": "2024-07-17T00:00:00.000Z", + "conversions": [ + { + "fieldNameFHIR": "performer.#:Organization.actor.identifier.value", + "fieldNameFlat": "SITE_CODE", + "expression": { + "expressionName": "Not Empty", + "expressionType": "NOTEMPTY", + "expressionRule": "" + } + }, + { + "fieldNameFHIR": "performer.#:Organization.actor.identifier.system", + "fieldNameFlat": "SITE_CODE_TYPE_URI", + "expression": { + "expressionName": "Defaults to", + "expressionType": "DEFAULT", + "expressionRule": "https://fhir.nhs.uk/Id/ods-organization-code" + } + } + ] +} diff --git a/delta_backend/tests/sample_data/fhir_sample.json b/delta_backend/tests/sample_data/fhir_sample.json index 3576196cd..23e778901 100644 --- a/delta_backend/tests/sample_data/fhir_sample.json +++ b/delta_backend/tests/sample_data/fhir_sample.json @@ -1,4 +1,3 @@ - { "resourceType": "Immunization", "contained": [ @@ -6,39 +5,33 @@ "resourceType": "Practitioner", "id": "Pract1", "name": [ - { - "family": "Furlong old 1", - "given": [ - "Darren 2", "old use value" - ], - "period": { - "start": "2000-01-01", - "end": "2030-01-01" - } - }, - { - "family": "Furlong old", - "given": [ - "Darren", "old use value" - ], - "use": "old", - "period": { - "start": "2000-01-01", - "end": "2030-01-01" - } - }, - { - "family": "Furlong official", - "given": [ - "Darren", "Official" - ], - "use": "official", - "period": { - "start": "2000-01-01", - "end": "2030-01-01" - } - } - ] + { + "family": "Furlong old 1", + "given": ["Darren 2", "old use value"], + "period": { + "start": "2000-01-01", + "end": "2030-01-01" + } + }, + { + "family": "Furlong old", + "given": ["Darren", "old use value"], + "use": "old", + "period": { + "start": "2000-01-01", + "end": "2030-01-01" + } + }, + { + "family": "Furlong official", + "given": ["Darren", "Official"], + "use": "official", + "period": { + "start": "2000-01-01", + "end": "2030-01-01" + } + } + ] }, { "resourceType": "Patient", @@ -50,106 +43,96 @@ } ], "name": [ - { - "use": "home", - "text": "hello pat1", - "family": "test10 ", - "given": [ - "test11", "test12", "test13" - ], - "period": { - "start" : "2000-01-01", - "end" : "2026-01-01" - } - }, - { - "use": "official", - "text": "hello pat2", - "family": "test12", - "given": [ - "test13", "test14", "test15" - ], - "period": { - "start" : "2000-01-01", - "end" : "2026-01-01" - } - }, - { - "use": "old", - "text": "hello pat3", - "family": "test14", - "given": [ - "test15", "test16", "test17" - ], - "period": { - "start" : "2000-01-01", - "end" : "2026-01-01" - } - } - ], + { + "use": "home", + "text": "hello pat1", + "family": "test10 ", + "given": ["test11", "test12", "test13"], + "period": { + "start": "2000-01-01", + "end": "2026-01-01" + } + }, + { + "use": "official", + "text": "hello pat2", + "family": "test12", + "given": ["test13", "test14", "test15"], + "period": { + "start": "2000-01-01", + "end": "2026-01-01" + } + }, + { + "use": "old", + "text": "hello pat3", + "family": "test14", + "given": ["test15", "test16", "test17"], + "period": { + "start": "2000-01-01", + "end": "2026-01-01" + } + } + ], "gender": "other", "birthDate": "2026-03-10", "address": [ - { - "use": "work", - "type": "both", - "text": "Validate Obf", - "line": [ - "1, work_2" - ], - "city": "work_3", - "district": "work_4", - "state": "work_5", - "country": "work_7", - "postalCode": "LS8 4ED", - "period": { - "start": "2000-01-01", - "end": "2030-01-01" - } - }, - { - "use": "Home", - "type": "Physical", - "text": "Validate Obf", - "line": [ - "1, obf_2" - ], - "city": "obf_3", - "district": "obf_4", - "state": "obf_5", - "postalCode": "WF8 4ED", - "country": "obf_7", - "period": { - "start": "2000-01-01", - "end": "2030-01-01" - } - } - ] + { + "use": "work", + "type": "both", + "text": "Validate Obf", + "line": ["1, work_2"], + "city": "work_3", + "district": "work_4", + "state": "work_5", + "country": "work_7", + "postalCode": "LS8 4ED", + "period": { + "start": "2000-01-01", + "end": "2030-01-01" + } + }, + { + "use": "Home", + "type": "Physical", + "text": "Validate Obf", + "line": ["1, obf_2"], + "city": "obf_3", + "district": "obf_4", + "state": "obf_5", + "postalCode": "WF8 4ED", + "country": "obf_7", + "period": { + "start": "2000-01-01", + "end": "2030-01-01" + } + } + ] } ], "extension": [ { "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", "valueCodeableConcept": { - "coding": [ - { - "code": "956951000000105", - "display": "Seasonal influenza vaccination (procedure)", - "system": "http://snomed.info/test" - }, - { - "code": "956951000000104", - "display": "Seasonal influenza vaccination (procedure)", - "system": "http://snomed.info/sct" - }, - { - "code": "NEG", - "display": "Seasonal influenza vaccination (procedure)", - "system": "https://acme.lab/resultcodes" - } - ] - } + "coding": [ + { + "code": "956951000000105", + "display": "Seasonal influenza vaccination (procedure)", + "system": "http://snomed.info/test" + }, + { + "code": "956951000000104", + "display": "Seasonal influenza vaccination (procedure)", + "system": "http://snomed.info/sct" + }, + { + "code": "NEG", + "display": "Seasonal influenza vaccination (procedure)", + "system": "https://acme.lab/resultcodes" + } + ] } + } ], "identifier": [ { @@ -257,4 +240,4 @@ "doseNumberPositiveInt": 2 } ] -} \ No newline at end of file +} diff --git a/delta_backend/tests/sample_data/vaccination.json b/delta_backend/tests/sample_data/vaccination.json index 7065fcf16..a4f997d3a 100644 --- a/delta_backend/tests/sample_data/vaccination.json +++ b/delta_backend/tests/sample_data/vaccination.json @@ -1,131 +1,129 @@ -{ - "resourceType": "Immunization", - "id": "d11c69d8-7a50-4a54-a848-7648121e995f", - "contained": [ - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference" : "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "ml", - "system": "http://snomed.info/sct", - "code": "258773002" - }, - "performer": [ - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] -} \ No newline at end of file +{ + "resourceType": "Immunization", + "id": "d11c69d8-7a50-4a54-a848-7648121e995f", + "contained": [ + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "ml", + "system": "http://snomed.info/sct", + "code": "258773002" + }, + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination2.json b/delta_backend/tests/sample_data/vaccination2.json index c58722cb6..b7e56a6d2 100644 --- a/delta_backend/tests/sample_data/vaccination2.json +++ b/delta_backend/tests/sample_data/vaccination2.json @@ -1,115 +1,113 @@ -{ - "resourceType": "Immunization", - "id": "d11c69d8-7a50-4a54-a848-7648121e995f", - "contained": [ - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference" : "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "doseQuantity": { - "value": 0.5, - "unit": "ml", - "system": "http://snomed.info/sct", - "code": "258773002" - }, - "performer": [ - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "resourceType": "Immunization", + "id": "d11c69d8-7a50-4a54-a848-7648121e995f", + "contained": [ + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "doseQuantity": { + "value": 0.5, + "unit": "ml", + "system": "http://snomed.info/sct", + "code": "258773002" + }, + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination3.json b/delta_backend/tests/sample_data/vaccination3.json index 7005060e6..691e58554 100644 --- a/delta_backend/tests/sample_data/vaccination3.json +++ b/delta_backend/tests/sample_data/vaccination3.json @@ -1,148 +1,146 @@ -{ - "id": "f219e05d-f4aa-4b46-a1b3-5e977fd7671c", - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1" - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000019" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Jonjo" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "822851000000102", - "display": "Seasonal influenza vaccination (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "ACME-vacc123flu" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39566211000001103", - "display": "Supemtek Quadrivalent vaccine (recombinant) solution for injection 0.5ml pre-filled syringes (Sanofi) (product)" - } - ] - }, - "patient": { - "reference" : "#Pat1" - }, - "occurrenceDateTime": "2021-02-14T13:28:17.271+00:00", - "recorded": "2021-02-14", - "primarySource": true, - "manufacturer": { - "display": "Sanofi" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "41925TJ61", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368209003", - "display": "Right upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "171279008", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "6142004", - "display": "Influenza (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "id": "f219e05d-f4aa-4b46-a1b3-5e977fd7671c", + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1" + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000019" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Jonjo"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "822851000000102", + "display": "Seasonal influenza vaccination (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123flu" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39566211000001103", + "display": "Supemtek Quadrivalent vaccine (recombinant) solution for injection 0.5ml pre-filled syringes (Sanofi) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-14T13:28:17.271+00:00", + "recorded": "2021-02-14", + "primarySource": true, + "manufacturer": { + "display": "Sanofi" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "41925TJ61", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368209003", + "display": "Right upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "171279008", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "6142004", + "display": "Influenza (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination4.json b/delta_backend/tests/sample_data/vaccination4.json index 2287599c5..91aabf2ed 100644 --- a/delta_backend/tests/sample_data/vaccination4.json +++ b/delta_backend/tests/sample_data/vaccination4.json @@ -1,150 +1,146 @@ -{ - "resourceType": "Immunization", - "id": "d11c69d8-7a50-4a54-a848-7648121e995f", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference" : "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "ml", - "system": "http://snomed.info/sct", - "code": "258773002" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "resourceType": "Immunization", + "id": "d11c69d8-7a50-4a54-a848-7648121e995f", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "ml", + "system": "http://snomed.info/sct", + "code": "258773002" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination5.json b/delta_backend/tests/sample_data/vaccination5.json index 917ab608b..7c417961f 100644 --- a/delta_backend/tests/sample_data/vaccination5.json +++ b/delta_backend/tests/sample_data/vaccination5.json @@ -1,173 +1,169 @@ -{ - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "ACME-vacc123456" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - }, - { - "system": "http://dm+d.org", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", - "userSelected": "true" - } - ], - "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" - }, - "patient": { - "reference" : "#Pat1", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - }, - "display": "TAYLOR, Sarah" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1", - "identifier": { - "system": "https://fhir.hl7.org.uk/Id/nmc-number", - "value": "5566789" - }, - "display": "NIGHTINGALE, Florence" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123456" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + }, + { + "system": "http://dm+d.org", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", + "userSelected": "true" + } + ], + "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" + }, + "patient": { + "reference": "#Pat1", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + }, + "display": "TAYLOR, Sarah" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1", + "identifier": { + "system": "https://fhir.hl7.org.uk/Id/nmc-number", + "value": "5566789" + }, + "display": "NIGHTINGALE, Florence" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination6.json b/delta_backend/tests/sample_data/vaccination6.json index 64e7ad111..015efb54f 100644 --- a/delta_backend/tests/sample_data/vaccination6.json +++ b/delta_backend/tests/sample_data/vaccination6.json @@ -1,225 +1,214 @@ -{ - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - }, - { - "system": "https://supplierABC/patientIndex", - "value": "X12841" - } - ], - "name": [ - { - "use": "official", - "family": "Taylor", - "given": [ - "Sarah", - "Jane" - ] - }, - { - "use": "maiden", - "family": "Barnes", - "given": [ - "Sarah", - "Jane" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - }, - { - "resourceType": "Location", - "id": "Loc1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/ods-site-code", - "value": "RR813" - } - ], - "name": "ST JAMES'S UNIVERSITY HOSPITAL", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "HOSP", - "display": "Hospital" - } - ] - } - ], - "telecom": [ - { - "system": "phone", - "value": "0113 243 3144" - } - ], - "address": { - "line": [ - "ST. JAMES'S UNIVERSITY HOSPITAL", - "BECKETT STREET" - ], - "city": "LEEDS", - "postalCode": "LS9 7TF", - "country": "ENGLAND" - } - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "ACME-vacc123456" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - }, - { - "system": "http://dm+d.org", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", - "userSelected": "true" - } - ], - "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" - }, - "patient": { - "reference" : "#Pat1", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - }, - "display": "TAYLOR, Sarah" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "reference" : "#Loc1", - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1", - "identifier": { - "system": "https://fhir.hl7.org.uk/Id/nmc-number", - "value": "5566789" - }, - "display": "NIGHTINGALE, Florence" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + }, + { + "system": "https://supplierABC/patientIndex", + "value": "X12841" + } + ], + "name": [ + { + "use": "official", + "family": "Taylor", + "given": ["Sarah", "Jane"] + }, + { + "use": "maiden", + "family": "Barnes", + "given": ["Sarah", "Jane"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + }, + { + "resourceType": "Location", + "id": "Loc1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ods-site-code", + "value": "RR813" + } + ], + "name": "ST JAMES'S UNIVERSITY HOSPITAL", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "HOSP", + "display": "Hospital" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "0113 243 3144" + } + ], + "address": { + "line": ["ST. JAMES'S UNIVERSITY HOSPITAL", "BECKETT STREET"], + "city": "LEEDS", + "postalCode": "LS9 7TF", + "country": "ENGLAND" + } + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123456" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + }, + { + "system": "http://dm+d.org", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", + "userSelected": "true" + } + ], + "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" + }, + "patient": { + "reference": "#Pat1", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + }, + "display": "TAYLOR, Sarah" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "reference": "#Loc1", + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1", + "identifier": { + "system": "https://fhir.hl7.org.uk/Id/nmc-number", + "value": "5566789" + }, + "display": "NIGHTINGALE, Florence" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/sample_data/vaccination7.json b/delta_backend/tests/sample_data/vaccination7.json index 43db7280c..ccc280d99 100644 --- a/delta_backend/tests/sample_data/vaccination7.json +++ b/delta_backend/tests/sample_data/vaccination7.json @@ -1,197 +1,189 @@ -{ - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id" : "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - }, - { - "system": "https://supplierABC/patientIndex", - "value": "X12841" - } - ], - "name": [ - { - "use": "maiden", - "family": "Barnes", - "given": [ - "Sarah", - "Jane" - ] - }, - { - "use": "official", - "family": "Taylor", - "given": [ - "Sarah", - "Jane" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "use": "old", - "text": "No fixed abode" - }, - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", - "valueId": "2837371000000119" - } - ], - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "ACME-vacc123456" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://dm+d.org", - "code": "39116211000001106", - "display": "Generic COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (product)", - "userSelected": "true" - }, - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ], - "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" - }, - "patient": { - "reference" : "#Pat1", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - }, - "display": "TAYLOR, Sarah" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference" : "#Pract1", - "identifier": { - "system": "https://fhir.hl7.org.uk/Id/nmc-number", - "value": "5566789" - }, - "display": "NIGHTINGALE, Florence" - } - }, - { - "actor" : { - "type" : "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } \ No newline at end of file +{ + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + }, + { + "system": "https://supplierABC/patientIndex", + "value": "X12841" + } + ], + "name": [ + { + "use": "maiden", + "family": "Barnes", + "given": ["Sarah", "Jane"] + }, + { + "use": "official", + "family": "Taylor", + "given": ["Sarah", "Jane"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "use": "old", + "text": "No fixed abode" + }, + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", + "valueId": "2837371000000119" + } + ], + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123456" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://dm+d.org", + "code": "39116211000001106", + "display": "Generic COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (product)", + "userSelected": "true" + }, + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ], + "text": "AstraZeneca UK Ltd Vaxzevria 0.5ml dose suspension for injection" + }, + "patient": { + "reference": "#Pat1", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + }, + "display": "TAYLOR, Sarah" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1", + "identifier": { + "system": "https://fhir.hl7.org.uk/Id/nmc-number", + "value": "5566789" + }, + "display": "NIGHTINGALE, Florence" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] +} diff --git a/delta_backend/tests/test_convert.py b/delta_backend/tests/test_convert.py index a8e2d2025..08c05d7f6 100644 --- a/delta_backend/tests/test_convert.py +++ b/delta_backend/tests/test_convert.py @@ -3,11 +3,10 @@ import unittest from copy import deepcopy from datetime import datetime -from unittest.mock import patch, Mock +from unittest.mock import patch from moto import mock_aws from boto3 import resource as boto3_resource from utils_for_converter_tests import ValuesForTests, ErrorValuesForTests -from converter import Converter from common.mappings import ActionFlag, Operation, EventName MOCK_ENV_VARS = { @@ -24,11 +23,12 @@ @patch.dict("os.environ", MOCK_ENV_VARS, clear=True) class TestConvertToFlatJson(unittest.TestCase): maxDiff = None + def setUp(self): # Start moto AWS mocks self.mock = mock_aws() self.mock.start() - + """Set up mock DynamoDB table.""" self.dynamodb_resource = boto3_resource("dynamodb", "eu-west-2") self.table = self.dynamodb_resource.create_table( @@ -48,7 +48,10 @@ def setUp(self): "IndexName": "IdentifierGSI", "KeySchema": [{"AttributeName": "IdentifierPK", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, { "IndexName": "PatientGSI", @@ -57,7 +60,10 @@ def setUp(self): {"AttributeName": "SupplierSystem", "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, ], ) @@ -74,7 +80,7 @@ def tearDown(self): self.logger_exception_patcher.stop() self.logger_info_patcher.stop() self.mock_firehose_logger.stop() - + self.mock.stop() @staticmethod @@ -82,7 +88,15 @@ def get_event(event_name=EventName.CREATE, operation="operation", supplier="EMIS """Returns test event data.""" return ValuesForTests.get_event(event_name, operation, supplier) - def assert_dynamodb_record(self, operation_flag, action_flag, items, expected_values, expected_imms, response): + def assert_dynamodb_record( + self, + operation_flag, + action_flag, + items, + expected_values, + expected_imms, + response, + ): """ Asserts that a record with the expected structure exists in DynamoDB. Ignores the dynamically generated field PK. @@ -94,13 +108,11 @@ def assert_dynamodb_record(self, operation_flag, action_flag, items, expected_va unfiltered_items = [ {k: v for k, v in item.items()} for item in items - if item.get("Operation") == operation_flag - and item.get("Imms", {}).get("ACTION_FLAG") == action_flag + if item.get("Operation") == operation_flag and item.get("Imms", {}).get("ACTION_FLAG") == action_flag ] filtered_items = [ - {k: v for k, v in item.items() if k not in ["PK", "DateTimeStamp", "ExpiresAt"]} - for item in unfiltered_items + {k: v for k, v in item.items() if k not in ["PK", "DateTimeStamp", "ExpiresAt"]} for item in unfiltered_items ] self.assertGreater(len(filtered_items), 0, f"No matching item found for {operation_flag}") @@ -121,22 +133,6 @@ def assert_dynamodb_record(self, operation_flag, action_flag, items, expected_va expires_at = unfiltered_items[0]["ExpiresAt"] self.assertEqual(expires_at - date_time, expected_seconds) - def test_fhir_converter_json_direct_data(self): - """it should convert fhir json data to flat json""" - json_data = json.dumps(ValuesForTests.json_data) - - fhir_converter = Converter(json_data) - FlatFile = fhir_converter.run_conversion() - - flatJSON = json.dumps(FlatFile) - expected_imms_value = deepcopy(ValuesForTests.expected_imms2) # UPDATE is currently the default action-flag - expected_imms = json.dumps(expected_imms_value) - self.assertEqual(flatJSON, expected_imms) - - errorRecords = fhir_converter.get_error_records() - - self.assertEqual(len(errorRecords), 0) - def test_fhir_converter_json_direct_data(self): """it should convert fhir json data to flat json""" json_data = json.dumps(ValuesForTests.json_data) @@ -155,7 +151,10 @@ def test_fhir_converter_json_direct_data(self): def test_fhir_converter_json_error_scenario_reporting_on(self): """it should convert fhir json data to flat json - error scenarios""" - error_test_cases = [ErrorValuesForTests.missing_json, ErrorValuesForTests.json_dob_error] + error_test_cases = [ + ErrorValuesForTests.missing_json, + ErrorValuesForTests.json_dob_error, + ] for test_case in error_test_cases: json_data = json.dumps(test_case) @@ -167,10 +166,13 @@ def test_fhir_converter_json_error_scenario_reporting_on(self): # Check if bad data creates error records self.assertTrue(len(errorRecords) > 0) - + def test_fhir_converter_json_error_scenario_reporting_off(self): """it should convert fhir json data to flat json - error scenarios""" - error_test_cases = [ErrorValuesForTests.missing_json, ErrorValuesForTests.json_dob_error] + error_test_cases = [ + ErrorValuesForTests.missing_json, + ErrorValuesForTests.json_dob_error, + ] for test_case in error_test_cases: json_data = json.dumps(test_case) @@ -182,7 +184,7 @@ def test_fhir_converter_json_error_scenario_reporting_off(self): # Check if bad data creates error records self.assertTrue(len(errorRecords) == 0) - + def test_fhir_converter_json_incorrect_data_scenario_reporting_on(self): """it should convert fhir json data to flat json - error scenarios""" @@ -190,8 +192,7 @@ def test_fhir_converter_json_incorrect_data_scenario_reporting_on(self): fhir_converter = Converter(None) errorRecords = fhir_converter.get_error_records() self.assertTrue(len(errorRecords) > 0) - - + def test_fhir_converter_json_incorrect_data_scenario_reporting_off(self): """it should convert fhir json data to flat json - error scenarios""" @@ -205,7 +206,10 @@ def test_handler_imms_convert_to_flat_json(self): expected_action_flags = [ {"Operation": Operation.CREATE, "EXPECTED_ACTION_FLAG": ActionFlag.CREATE}, {"Operation": Operation.UPDATE, "EXPECTED_ACTION_FLAG": ActionFlag.UPDATE}, - {"Operation": Operation.DELETE_LOGICAL, "EXPECTED_ACTION_FLAG": ActionFlag.DELETE_LOGICAL}, + { + "Operation": Operation.DELETE_LOGICAL, + "EXPECTED_ACTION_FLAG": ActionFlag.DELETE_LOGICAL, + }, ] for test_case in expected_action_flags: @@ -228,7 +232,7 @@ def test_handler_imms_convert_to_flat_json(self): items, expected_values, expected_imms, - response + response, ) result = self.table.scan() @@ -240,8 +244,6 @@ def clear_table(self): with self.table.batch_writer() as batch: for item in scan.get("Items", []): batch.delete_item(Key={"PK": item["PK"]}) - result = self.table.scan() - items = result.get("Items", []) if __name__ == "__main__": unittest.main() diff --git a/delta_backend/tests/test_convert_dates.py b/delta_backend/tests/test_convert_dates.py index 52a69214a..15104f3ea 100644 --- a/delta_backend/tests/test_convert_dates.py +++ b/delta_backend/tests/test_convert_dates.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestDateConversions(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_date_test(self, flat_field_name, date): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) @@ -17,9 +18,9 @@ def _run_date_test(self, flat_field_name, date): self.assertEqual(flat_json.get(flat_field_name), date) def test_person_dob_converted_format(self): - expected_dob = "19650228" + expected_dob = "19650228" self._run_date_test(ConversionFieldName.PERSON_DOB, expected_dob) - + def test_person_dob_missing(self): # Remove birthDate from Patient resource for res in self.request_json_data["contained"]: @@ -33,7 +34,7 @@ def test_person_dob_empty(self): if res["resourceType"] == "Patient": res["birthDate"] = "" self._run_date_test(ConversionFieldName.PERSON_DOB, "") - + def test_recorded_date_converted_format(self): self._run_date_test(ConversionFieldName.RECORDED_DATE, "20210207") @@ -44,7 +45,7 @@ def test_recorded_date_missing(self): def test_recorded_date_empty(self): self.request_json_data["recorded"] = "" self._run_date_test(ConversionFieldName.RECORDED_DATE, "") - + def test_expiry_date_converted_format(self): self._run_date_test(ConversionFieldName.EXPIRY_DATE, "20210702") @@ -55,7 +56,7 @@ def test_expiry_date_missing(self): def test_expiry_date_empty(self): self.request_json_data["expirationDate"] = "" self._run_date_test(ConversionFieldName.EXPIRY_DATE, "") - + def test_date_and_time_with_utc(self): self.request_json_data["occurrenceDateTime"] = "2025-04-06T13:28:17+00:00" self._run_date_test(ConversionFieldName.DATE_AND_TIME, "20250406T13281700") @@ -79,4 +80,3 @@ def test_date_and_time_empty(self): def test_date_and_time_invalid_format(self): self.request_json_data["occurrenceDateTime"] = "not-a-date" self._run_date_test(ConversionFieldName.DATE_AND_TIME, "") - \ No newline at end of file diff --git a/delta_backend/tests/test_convert_dose_amount.py b/delta_backend/tests/test_convert_dose_amount.py index 171663ce5..e359c55c9 100644 --- a/delta_backend/tests/test_convert_dose_amount.py +++ b/delta_backend/tests/test_convert_dose_amount.py @@ -6,6 +6,7 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestDoseAmountTypeUriToFlatJson(unittest.TestCase): def setUp(self): @@ -23,14 +24,14 @@ def test_dose_amount_value_exists(self): self.request_json_data["doseQuantity"] = { "value": 0.5, "code": "ml", - "unit": "milliliter" + "unit": "milliliter", } self._run_dose_amount_test(expected_result=decimal.Decimal(0.5)) def test_dose_amount_value_missing(self): self.request_json_data["doseQuantity"] = { "code": "ml", - "unit": "milliliter" + "unit": "milliliter", # 'value' intentionally omitted } self._run_dose_amount_test(expected_result="") diff --git a/delta_backend/tests/test_convert_dose_sequence.py b/delta_backend/tests/test_convert_dose_sequence.py index 6a87c6764..99e1bc54d 100644 --- a/delta_backend/tests/test_convert_dose_sequence.py +++ b/delta_backend/tests/test_convert_dose_sequence.py @@ -5,23 +5,20 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestDoseSequenceToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_test(self, expected_result): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json[ConversionFieldName.DOSE_SEQUENCE], expected_result) - + def test_dose_sequence_present_int(self): - self.request_json_data["protocolApplied"] = [ - { - "doseNumberPositiveInt": 2 - } - ] + self.request_json_data["protocolApplied"] = [{"doseNumberPositiveInt": 2}] self._run_test(expected_result="2") def test_dose_sequence_missing(self): diff --git a/delta_backend/tests/test_convert_location_code.py b/delta_backend/tests/test_convert_location_code.py index d44dcf236..0dab0ed83 100644 --- a/delta_backend/tests/test_convert_location_code.py +++ b/delta_backend/tests/test_convert_location_code.py @@ -5,25 +5,26 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestLocationCode(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_location_code_test(self, expected_site_code): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(ConversionFieldName.LOCATION_CODE), expected_site_code) - + def test_location_code_when_present(self): """Should return the correct LOCATION_CODE from input""" self.request_json_data["location"] = { "identifier": { "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "ABC123" + "value": "ABC123", }, - "type": "Location" + "type": "Location", } self._run_location_code_test("ABC123") @@ -34,21 +35,13 @@ def test_location_code_when_missing(self): def test_location_code_when_identifier_missing(self): """Should return 'X99999' when location.identifier is missing""" - self.request_json_data["location"] = { - "type": "Location" - } + self.request_json_data["location"] = {"type": "Location"} self._run_location_code_test("X99999") def test_location_code_when_value_missing(self): """Should return 'X99999' when location.identifier.value is missing""" self.request_json_data["location"] = { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "type": "Location" + "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code"}, + "type": "Location", } self._run_location_code_test("X99999") - - - - \ No newline at end of file diff --git a/delta_backend/tests/test_convert_location_code_type_uri.py b/delta_backend/tests/test_convert_location_code_type_uri.py index 79863a396..718697109 100644 --- a/delta_backend/tests/test_convert_location_code_type_uri.py +++ b/delta_backend/tests/test_convert_location_code_type_uri.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestLocationCodeTypeUri(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_location_code_type_uri_test(self, expected_uri): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) @@ -21,9 +22,9 @@ def test_location_code_type_uri_when_present(self): self.request_json_data["location"] = { "identifier": { "system": "https://custom-url.org/LocationSystem", - "value": "ABC123" + "value": "ABC123", }, - "type": "Location" + "type": "Location", } self._run_location_code_type_uri_test("https://custom-url.org/LocationSystem") @@ -34,17 +35,13 @@ def test_location_code_type_uri_when_location_missing(self): def test_location_code_type_uri_when_identifier_missing(self): """Should return default LOCATION_CODE_TYPE_URI when identifier is missing""" - self.request_json_data["location"] = { - "type": "Location" - } + self.request_json_data["location"] = {"type": "Location"} self._run_location_code_type_uri_test("https://fhir.nhs.uk/Id/ods-organization-code") def test_location_code_type_uri_when_system_missing(self): """Should return default LOCATION_CODE_TYPE_URI when system is missing""" self.request_json_data["location"] = { - "identifier": { - "value": "ABC123" - }, - "type": "Location" + "identifier": {"value": "ABC123"}, + "type": "Location", } self._run_location_code_type_uri_test("https://fhir.nhs.uk/Id/ods-organization-code") diff --git a/delta_backend/tests/test_convert_lot_number.py b/delta_backend/tests/test_convert_lot_number.py index 291a50990..95ac7d6e4 100644 --- a/delta_backend/tests/test_convert_lot_number.py +++ b/delta_backend/tests/test_convert_lot_number.py @@ -5,17 +5,18 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestBatchNumber(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_batch_number_test(self, expected_result): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(ConversionFieldName.BATCH_NUMBER), expected_result) - + def test_batch_number_present(self): """Should extract lotNumber when present""" self.request_json_data["lotNumber"] = "4120Z001" @@ -30,5 +31,3 @@ def test_batch_number_empty_string(self): """Should return None when lotNumber is an empty string""" self.request_json_data["lotNumber"] = "" self._run_batch_number_test(expected_result="") - - \ No newline at end of file diff --git a/delta_backend/tests/test_convert_manufacturer.py b/delta_backend/tests/test_convert_manufacturer.py index 558eb7600..5089abbd4 100644 --- a/delta_backend/tests/test_convert_manufacturer.py +++ b/delta_backend/tests/test_convert_manufacturer.py @@ -5,17 +5,18 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestVaccineManufacturer(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_vaccine_manufacturer_test(self, expected_result): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(ConversionFieldName.VACCINE_MANUFACTURER), expected_result) - + def test_vaccine_manufacturer_present(self): """Should return the manufacturer name when present""" self.request_json_data["manufacturer"] = {"display": "AstraZeneca Ltd"} @@ -35,5 +36,3 @@ def test_vaccine_manufacturer_empty_string(self): """Should return None when manufacturer.display is an empty string""" self.request_json_data["manufacturer"] = {"display": ""} self._run_vaccine_manufacturer_test(expected_result="") - - \ No newline at end of file diff --git a/delta_backend/tests/test_convert_nhs_number.py b/delta_backend/tests/test_convert_nhs_number.py index 8a95a460f..57fb29198 100644 --- a/delta_backend/tests/test_convert_nhs_number.py +++ b/delta_backend/tests/test_convert_nhs_number.py @@ -5,36 +5,36 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestNHSNumberToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_nhs_number_test(self, expected_result): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(ConversionFieldName.NHS_NUMBER), expected_result) - + def test_nhs_number_valid(self): # Sample already contains valid NHS number in the Patient resource self._run_nhs_number_test(expected_result="9000000009") - + def test_nhs_number_invalid_system(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["identifier"][0]["system"] = "http://wrong-system.org" self._run_nhs_number_test(expected_result="") - + def test_nhs_number_missing_identifier(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource.pop("identifier", "") self._run_nhs_number_test(expected_result="") - + def test_nhs_number_missing_patient(self): self.request_json_data["contained"] = [ - r for r in self.request_json_data.get("contained", []) - if r.get("resourceType") != "Patient" + r for r in self.request_json_data.get("contained", []) if r.get("resourceType") != "Patient" ] - self._run_nhs_number_test(expected_result="") \ No newline at end of file + self._run_nhs_number_test(expected_result="") diff --git a/delta_backend/tests/test_convert_person_forename.py b/delta_backend/tests/test_convert_person_forename.py index e5787a81d..e813eba8b 100644 --- a/delta_backend/tests/test_convert_person_forename.py +++ b/delta_backend/tests/test_convert_person_forename.py @@ -5,18 +5,19 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPersonForenmeToFlatJson(unittest.TestCase): - """" + """ " Test cases for converting person forename to flat JSON format. ## 1. If there is only one name use that one, else ## 2. Select first name where Use=official with period covering vaccination date, else ## 3. select instance where current name with use!=old at vaccination date ## 4. Fallback to first available name instance """ - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_person_forename_multiple_names_official(self): """Test case where multiple name instances exist, and one has use=official with period covering vaccination date""" self.request_json_data["contained"][1]["name"] = [ @@ -51,7 +52,11 @@ def test_person_forename_multiple_names_official(self): def test_person_forename_multiple_names_current(self): """Test case where no official name is present, but a name is current at the vaccination date""" self.request_json_data["contained"][1]["name"] = [ - {"family": "Doe", "given": ["John"], "period": {"start": "2020-01-01", "end": "2023-01-01"}}, + { + "family": "Doe", + "given": ["John"], + "period": {"start": "2020-01-01", "end": "2023-01-01"}, + }, {"family": "Doe", "given": ["Johnny"], "use": "nickname"}, ] expected_forename = "John" @@ -66,7 +71,12 @@ def test_person_forename_single_name(self): def test_person_forename_no_official_but_current_not_old(self): """Test case where no official name is present, but a current name with use!=old exists at vaccination date""" self.request_json_data["contained"][1]["name"] = [ - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Doe", "given": ["Chris"], @@ -81,7 +91,12 @@ def test_person_forename_fallback_to_first_name(self): """Test case where no names match the previous conditions, fallback to first available name""" self.request_json_data["contained"][1]["name"] = [ {"family": "Doe", "given": ["Elliot"], "use": "nickname"}, - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Doe", "given": ["Chris"], @@ -139,30 +154,30 @@ def test_person_forename_exists_only(self): ] expected_forename = "" self._run_test(expected_forename) - + def test_person_forename_names_not_provided(self): """Test case where the selected name has multiple given names""" self.request_json_data["contained"][1]["name"] = [] self._run_test("") - + def test_person_forename_exists_only_in_one_instance(self): """Test case where the selected name has multiple given names""" self.request_json_data["contained"][1]["name"] = [ { "given": ["Jack"], "use": "official", - "period": { "start": "2021-01-01", "end": "2023-01-01" } + "period": {"start": "2021-01-01", "end": "2023-01-01"}, }, { "family": "Smith", "given": ["Alex"], "use": "official", - "period": { "start": "2021-01-01", "end": "2023-01-01" } - } + "period": {"start": "2021-01-01", "end": "2023-01-01"}, + }, ] expected_forename = "Alex" self._run_test(expected_forename) - + def _run_test(self, expected_forename): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) diff --git a/delta_backend/tests/test_convert_person_gender.py b/delta_backend/tests/test_convert_person_gender.py index 7f2b6357b..064c1e7b9 100644 --- a/delta_backend/tests/test_convert_person_gender.py +++ b/delta_backend/tests/test_convert_person_gender.py @@ -5,49 +5,50 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPersonGenderToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _run_test(self, expected_result): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json[ConversionFieldName.PERSON_GENDER_CODE], expected_result) - + def test_gender_male(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["gender"] = "male" self._run_test(expected_result="1") - + def test_gender_female(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["gender"] = "female" self._run_test(expected_result="2") - + def test_gender_other(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["gender"] = "other" self._run_test(expected_result="9") - + def test_gender_unknown(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["gender"] = "unknown" self._run_test(expected_result="0") - + def test_gender_missing(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource.pop("gender", "") self._run_test(expected_result="") - + def test_gender_invalid(self): for resource in self.request_json_data.get("contained", []): if resource.get("resourceType") == "Patient": resource["gender"] = "random" - self._run_test(expected_result="") \ No newline at end of file + self._run_test(expected_result="") diff --git a/delta_backend/tests/test_convert_person_surname.py b/delta_backend/tests/test_convert_person_surname.py index f77ea5791..8d1331abe 100644 --- a/delta_backend/tests/test_convert_person_surname.py +++ b/delta_backend/tests/test_convert_person_surname.py @@ -5,8 +5,9 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPersonSurnameToFlatJson(unittest.TestCase): - """" + """ " Test cases for converting person surname to flat JSON format. ## 1. If there is only one name use that one, else ## 2. Select first name where Use=official with period covering vaccination date, else @@ -16,7 +17,7 @@ class TestPersonSurnameToFlatJson(unittest.TestCase): def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_person_surname_multiple_names_official(self): """Test case where multiple name instances exist, and one has use=official with period covering vaccination date""" self.request_json_data["contained"][1]["name"] = [ @@ -51,7 +52,11 @@ def test_person_surname_multiple_names_official(self): def test_person_surname_multiple_names_current(self): """Test case where no official name is present, but a name is current at the vaccination date""" self.request_json_data["contained"][1]["name"] = [ - {"family": "Manny", "given": ["John"], "period": {"start": "2020-01-01", "end": "2023-01-01"}}, + { + "family": "Manny", + "given": ["John"], + "period": {"start": "2020-01-01", "end": "2023-01-01"}, + }, {"family": "Doe", "given": ["Johnny"], "use": "nickname"}, ] expected_surname = "Manny" @@ -83,7 +88,12 @@ def test_person_surname_single_name(self): def test_person_surname_no_official_but_current_not_old(self): """Test case where no official name is present, but a current name with use!=old exists at vaccination date""" self.request_json_data["contained"][1]["name"] = [ - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Manny", "given": ["Chris"], @@ -131,45 +141,34 @@ def test_person_surname_exists_only_in_one_instance(self): { "family": "Doe", "use": "official", - "period": { "start": "2021-01-01", "end": "2023-01-01" } + "period": {"start": "2021-01-01", "end": "2023-01-01"}, }, { "family": "Smith", "given": ["Jack"], "use": "official", - "period": { "start": "2021-01-01", "end": "2023-01-01" } - } + "period": {"start": "2021-01-01", "end": "2023-01-01"}, + }, ] expected_surname = "Smith" self._run_test_surname(expected_surname) - + def test_person_surname_and_forename_exists_only(self): """Test case where only family and given properties exist""" - self.request_json_data["contained"][1]["name"] = [ - { - "family": "Doe", - "given": ["Test"] - } - ] + self.request_json_data["contained"][1]["name"] = [{"family": "Doe", "given": ["Test"]}] expected_surname = "Doe" self._run_test_surname(expected_surname) - + def test_person_returns_first_name_as_fallback(self): """Test fallback to names[0] when no official or valid names with both given and family exist""" self.request_json_data["contained"][1]["name"] = [ - { - "use": "nickname", - "given": ["OnlyGiven"] - }, - { - "use": "old", - "family": "OldFamily" - } + {"use": "nickname", "given": ["OnlyGiven"]}, + {"use": "old", "family": "OldFamily"}, ] - - expected_surname = "" + + expected_surname = "" self._run_test_surname(expected_surname) - + def _run_test_surname(self, expected_surname): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) diff --git a/delta_backend/tests/test_convert_post_code.py b/delta_backend/tests/test_convert_post_code.py index 1e149d0d4..a07dc6d63 100644 --- a/delta_backend/tests/test_convert_post_code.py +++ b/delta_backend/tests/test_convert_post_code.py @@ -1,36 +1,40 @@ import copy -import datetime import json import unittest from utils_for_converter_tests import ValuesForTests from converter import Converter from common.mappings import ConversionFieldName + class TestPersonPostalCodeToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - - def test_person_postal_code_not_valid_object(self): + + def test_person_postal_code_not_valid_object(self): self.request_json_data["contained"][1]["address"] = {} expected_postal_code = "ZZ99 3CZ" self._run_postal_code_test(expected_postal_code) - + def test_person_postal_code_single_address(self): """Test case where only one address instance exists""" - self.request_json_data["contained"][1]["address"] = [{ + self.request_json_data["contained"][1]["address"] = [ + { "postalCode": "AB12 3CD", "use": "home", "type": "physical", "period": {"start": "2018-01-01", "end": "2020-12-31"}, - }] + } + ] def test_person_postal_code_single_address_only_postal_code(self): """Test case where only one address instance exists with one postalCode""" - self.request_json_data["contained"][1]["address"] = [{ - "postalCode": "AB12 3CD", - }] - + self.request_json_data["contained"][1]["address"] = [ + { + "postalCode": "AB12 3CD", + } + ] + expected_postal_code = "AB12 3CD" self._run_postal_code_test(expected_postal_code) @@ -114,7 +118,7 @@ def test_person_postal_code_case_insensitive_match(self): "use": "Home", # capital H "type": "Physical", # capital P "period": {"start": "2000-01-01", "end": "2023-01-01"}, - } + }, ] expected_postal_code = "WF8 4ED" self._run_postal_code_test(expected_postal_code) @@ -131,22 +135,20 @@ def test_person_postal_code_default_to_ZZ99_3CZ(self): def test_person_postal_code_blank_string_should_fallback(self): """Test case where postalCode is an empty string — should fallback to ZZ99 3CZ""" self.request_json_data["contained"][1]["address"] = [ - {"postalCode": "", - "use": "home", - "type": "physical", - "period": {"start": "2018-01-01", "end": "2030-12-31"}, - }, + { + "postalCode": "", + "use": "home", + "type": "physical", + "period": {"start": "2018-01-01", "end": "2030-12-31"}, + }, ] expected_postal_code = "ZZ99 3CZ" self._run_postal_code_test(expected_postal_code) assert "postalCode" in self.request_json_data["contained"][1]["address"][0] assert self.request_json_data["contained"][1]["address"][0]["postalCode"] == "" - - def _run_postal_code_test(self, expected_postal_code): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json[ConversionFieldName.PERSON_POSTCODE], expected_postal_code) - diff --git a/delta_backend/tests/test_convert_practitioner_forename.py b/delta_backend/tests/test_convert_practitioner_forename.py index f447cecbf..38edc88b9 100644 --- a/delta_backend/tests/test_convert_practitioner_forename.py +++ b/delta_backend/tests/test_convert_practitioner_forename.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPractitionerForenameToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_practitioner_forename_multiple_names_official(self): """Test case where multiple name instances exist, and one has use=official with period covering vaccination date""" self.request_json_data["contained"][0]["name"] = [ @@ -44,7 +45,11 @@ def test_practitioner_forename_multiple_names_official(self): def test_practitioner_forename_multiple_names_current(self): """Test case where no official name is present, but a name is current at the vaccination date""" self.request_json_data["contained"][0]["name"] = [ - {"family": "Doe", "given": ["John"], "period": {"start": "2020-01-01", "end": "2023-01-01"}}, + { + "family": "Doe", + "given": ["John"], + "period": {"start": "2020-01-01", "end": "2023-01-01"}, + }, {"family": "Doe", "given": ["Johnny"], "use": "nickname"}, ] expected_forename = "John" @@ -59,7 +64,12 @@ def test_practitioner_forename_single_name(self): def test_practitioner_forename_no_official_but_current_not_old(self): """Test case where no official name is present, but a current name with use!=old exists at vaccination date""" self.request_json_data["contained"][0]["name"] = [ - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Doe", "given": ["Chris"], @@ -74,7 +84,12 @@ def test_practitioner_forename_fallback_to_first_name(self): """Test case where no names match the previous conditions, fallback to first available name""" self.request_json_data["contained"][0]["name"] = [ {"family": "Doe", "given": ["Elliot"], "use": "nickname"}, - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Doe", "given": ["Chris"], @@ -107,7 +122,11 @@ def test_practitioner_forename_multiple_given_names_concatenation(self): def test_practitioner_forename_given_missing(self): """Test case where the selected name has multiple given names""" self.request_json_data["contained"][0]["name"] = [ - {"family": "Doe", "use": "official", "period": {"start": "2021-01-01", "end": "2022-12-31"}} + { + "family": "Doe", + "use": "official", + "period": {"start": "2021-01-01", "end": "2022-12-31"}, + } ] expected_forename = "" self._run_practitioner_test(expected_forename) @@ -121,22 +140,28 @@ def test_practitioner_forename_empty(self): def test_practitioner_forename_exists_only_and_official(self): """Test case where the selected name has multiple given names""" self.request_json_data["contained"][0]["name"] = [ - {"given" : ["test"], "use": "official", "period": {"start": "2021-01-01", "end": "2022-12-31"}} + { + "given": ["test"], + "use": "official", + "period": {"start": "2021-01-01", "end": "2022-12-31"}, + } ] expected_forename = "test" self._run_practitioner_test(expected_forename) - + def test_practitioner_forename_exists_only_and_not_official(self): """Test case where the selected name has multiple given names""" self.request_json_data["contained"][0]["name"] = [ - {"given" : ["test"], "period": {"start": "2021-01-01", "end": "2022-12-31"}} + {"given": ["test"], "period": {"start": "2021-01-01", "end": "2022-12-31"}} ] expected_forename = "test" self._run_practitioner_test(expected_forename) - + def _run_practitioner_test(self, expected_forename): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() - self.assertEqual(flat_json[ConversionFieldName.PERFORMING_PROFESSIONAL_FORENAME], expected_forename) - + self.assertEqual( + flat_json[ConversionFieldName.PERFORMING_PROFESSIONAL_FORENAME], + expected_forename, + ) diff --git a/delta_backend/tests/test_convert_practitioner_surname.py b/delta_backend/tests/test_convert_practitioner_surname.py index da95478cd..b8bf1919b 100644 --- a/delta_backend/tests/test_convert_practitioner_surname.py +++ b/delta_backend/tests/test_convert_practitioner_surname.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPractitionerSurnameToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_practitioner_surname_multiple_names_official(self): """Test case where multiple name instances exist, and one has use=official with period covering vaccination date""" self.request_json_data["contained"][0]["name"] = [ @@ -44,7 +45,11 @@ def test_practitioner_surname_multiple_names_official(self): def test_practitioner_surname_multiple_names_current(self): """Test case where no official name is present, but a name is current at the vaccination date""" self.request_json_data["contained"][0]["name"] = [ - {"family": "Manny", "given": ["John"], "period": {"start": "2020-01-01", "end": "2023-01-01"}}, + { + "family": "Manny", + "given": ["John"], + "period": {"start": "2020-01-01", "end": "2023-01-01"}, + }, {"family": "Doe", "given": ["Johnny"], "use": "nickname"}, ] expected_surname = "Manny" @@ -59,7 +64,12 @@ def test_practitioner_surname_single_name(self): def test_practitioner_surname_no_official_but_current_not_old(self): """Test case where no official name is present, but a current name with use!=old exists at vaccination date""" self.request_json_data["contained"][0]["name"] = [ - {"family": "Doe", "given": ["John"], "use": "old", "period": {"start": "2018-01-01", "end": "2020-12-31"}}, + { + "family": "Doe", + "given": ["John"], + "use": "old", + "period": {"start": "2018-01-01", "end": "2020-12-31"}, + }, { "family": "Manny", "given": ["Chris"], @@ -95,7 +105,7 @@ def test_contained_empty(self): self.request_json_data["contained"][0]["name"] = [] expected_surname = "" self._run_test_practitioner_surname(expected_surname) - + def test_contained_only_surname(self): """Test case where no names match the previous conditions, fallback to first available name""" self.request_json_data["contained"][0]["name"] = [{"family": "Doe", "use": "official"}] @@ -106,4 +116,7 @@ def _run_test_practitioner_surname(self, expected_surname): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() - self.assertEqual(flat_json[ConversionFieldName.PERFORMING_PROFESSIONAL_SURNAME], expected_surname) + self.assertEqual( + flat_json[ConversionFieldName.PERFORMING_PROFESSIONAL_SURNAME], + expected_surname, + ) diff --git a/delta_backend/tests/test_convert_primary_source.py b/delta_backend/tests/test_convert_primary_source.py index ffbfe5be4..e5b94b261 100644 --- a/delta_backend/tests/test_convert_primary_source.py +++ b/delta_backend/tests/test_convert_primary_source.py @@ -5,6 +5,7 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestPrimarySourceFlatJson(unittest.TestCase): def setUp(self): diff --git a/delta_backend/tests/test_convert_site_code.py b/delta_backend/tests/test_convert_site_code.py index 5b9fa3132..5eab55b0b 100644 --- a/delta_backend/tests/test_convert_site_code.py +++ b/delta_backend/tests/test_convert_site_code.py @@ -5,31 +5,34 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestSiteCodeToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_site_code_not_exists(self): self.request_json_data["performer"] = None self._run_site_code_test("") - + def test_site_code_actor_empty(self): self.request_json_data["performer"] = [{"actor": {}}] self._run_site_code_test("") - + def test_site_code_single_performer(self): """Test case where only one performer instance exists""" self.request_json_data["performer"] = [ { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, } }, {"actor": {"reference": "#Pract1"}}, ] - {"actor": {"value": "OTHER123"}}, expected_site_code = "B0C4P" self._run_site_code_test(expected_site_code) @@ -38,19 +41,28 @@ def test_site_code_performer_type_organization_only(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code1", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code2", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code3"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code3", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -63,19 +75,28 @@ def test_site_code_performer_type_organization(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organizatdion-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organizatdion-code", + "value": "code1", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code2", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhss-code", "value": "code3"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhss-code", + "value": "code3", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -88,22 +109,34 @@ def test_site_code_performer_type_without_oraganisation(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code2", + }, } }, { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code1", + }, } }, { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code4"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code4", + }, } }, { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhss-code", "value": "code3"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhss-code", + "value": "code3", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -116,12 +149,18 @@ def test_site_code_fallback_to_first_performer(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code1", + }, } }, { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhss-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhss-code", + "value": "code2", + }, } }, {"actor": {"reference": "#Pract1"}}, diff --git a/delta_backend/tests/test_convert_site_uri.py b/delta_backend/tests/test_convert_site_uri.py index 42a0f3d8e..6d29b25e3 100644 --- a/delta_backend/tests/test_convert_site_uri.py +++ b/delta_backend/tests/test_convert_site_uri.py @@ -5,23 +5,26 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestSiteUriToFlatJson(unittest.TestCase): - + def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def test_site_uri_single_performer(self): """Test case where only one performer instance exists""" self.request_json_data["performer"] = [ { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, } }, {"actor": {"reference": "#Pract1"}}, ] - {"actor": {"value": "OTHER123"}}, expected_site_uri = "https://fhir.nhs.uk/Id/ods-organization-code" self._run_site_uri_test(expected_site_uri) @@ -30,19 +33,28 @@ def test_site_code_performer_type_organization_only(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-codes", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-codes", + "value": "code1", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "code2", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code3"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code3", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -55,19 +67,28 @@ def test_site_code_performer_type_organization(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organizatdion-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organizatdion-code", + "value": "code1", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code2", + }, } }, { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhss-code", "value": "code3"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhss-code", + "value": "code3", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -80,12 +101,18 @@ def test_site_code_fallback_to_first_performer(self): self.request_json_data["performer"] = [ { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhs-code", "value": "code1"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhs-code", + "value": "code1", + }, } }, { "actor": { - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-nhss-code", "value": "code2"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-nhss-code", + "value": "code2", + }, } }, {"actor": {"reference": "#Pract1"}}, @@ -98,4 +125,3 @@ def _run_site_uri_test(self, expected_site_code): self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(ConversionFieldName.SITE_CODE_TYPE_URI), expected_site_code) - diff --git a/delta_backend/tests/test_convert_snomed_codes.py b/delta_backend/tests/test_convert_snomed_codes.py index a246f01d2..593eb08b4 100644 --- a/delta_backend/tests/test_convert_snomed_codes.py +++ b/delta_backend/tests/test_convert_snomed_codes.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestSNOMEDToFlatJson(unittest.TestCase): def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _set_snomed_codings(self, target_path: str, codings: list[dict], extension_url: str = None): """Helper to insert coding entries into self.request_json_data at the desired FHIR path""" if target_path in {"vaccineCode", "site", "route"}: @@ -17,246 +18,373 @@ def _set_snomed_codings(self, target_path: str, codings: list[dict], extension_u elif target_path == "reasonCode": self.request_json_data["reasonCode"] = [{"coding": codings}] elif target_path == "extension": - self.request_json_data["extension"] = [{ - "url": extension_url or "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": codings + self.request_json_data["extension"] = [ + { + "url": extension_url + or "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": {"coding": codings}, } - }] + ] def _run_snomed_test(self, flat_field_name, expected_snomed_code): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(flat_field_name), expected_snomed_code) - + def test_vaccination_procedure_code_no_matching_extension_url_returns_empty(self): self.request_json_data["extension"] = [ { "url": "https://wrong.url", - "valueCodeableConcept": { - "coding": [ - {"code": "123", "system": "http://snomed.info/sct"} - ] - } + "valueCodeableConcept": {"coding": [{"code": "123", "system": "http://snomed.info/sct"}]}, } ] self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "") - + def test_vaccination_procedure_code_empty_coding_returns_empty(self): self._set_snomed_codings("extension", []) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "") - + def test_vaccination_procedure_code_no_snomed_system_returns_empty(self): - self._set_snomed_codings("extension", [ - {"code": "999", "system": "http://example.com/other"} - ]) + self._set_snomed_codings("extension", [{"code": "999", "system": "http://example.com/other"}]) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "") - + def test_vaccination_procedure_code_missing_code_field_returns_empty(self): - self._set_snomed_codings("extension", [ - {"system": "http://snomed.info/sct", "display": "No code"} - ]) + self._set_snomed_codings("extension", [{"system": "http://snomed.info/sct", "display": "No code"}]) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "") - + def test_vaccination_procedure_code_correct_extension_url_matched(self): self.request_json_data["extension"] = [ { "url": "https://wrong.url", "valueCodeableConcept": { "coding": [ - {"code": "1324681000000101", "system": "http://snomed.info/sct", "display": "..."} + { + "code": "1324681000000101", + "system": "http://snomed.info/sct", + "display": "...", + } ] - } + }, }, { "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", "valueCodeableConcept": { "coding": [ - {"code": "1324681000000102", "system": "http://snomed.info/sct", "display": "..."} + { + "code": "1324681000000102", + "system": "http://snomed.info/sct", + "display": "...", + } ] - } - } + }, + }, ] self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "1324681000000102") - + def test_vaccination_procedure_code_single_coding_returns_first_code(self): - self._set_snomed_codings("extension", [ - {"code": "1324681000000101", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "extension", + [ + { + "code": "1324681000000101", + "system": "http://snomed.info/sct", + "display": "...", + } + ], + ) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "1324681000000101") - def test_vaccination_procedure_code_double_coding_and_incorrect_system_returns_correct_code(self): - self._set_snomed_codings("extension", [ - {"code": "1324681000000101", "system": "http://snomed.info/invalid", "display": "..."}, - {"code": "1324681000000102", "system": "http://snomed.info/sct", "display": "..."} - ]) + def test_vaccination_procedure_code_double_coding_and_incorrect_system_returns_correct_code( + self, + ): + self._set_snomed_codings( + "extension", + [ + { + "code": "1324681000000101", + "system": "http://snomed.info/invalid", + "display": "...", + }, + { + "code": "1324681000000102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "1324681000000102") def test_vaccination_procedure_code_double_coding_returns_first_code(self): - self._set_snomed_codings("extension", [ - {"code": "1324681000000101", "system": "http://snomed.info/sct", "display": "..."}, - {"code": "1324681000000102", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "extension", + [ + { + "code": "1324681000000101", + "system": "http://snomed.info/sct", + "display": "...", + }, + { + "code": "1324681000000102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_CODE, "1324681000000101") def test_vaccine_product_code_missing_field_returns_empty(self): self.request_json_data.pop("vaccineCode", None) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "") - + def test_vaccine_product_code_no_snomed_returns_empty(self): - self._set_snomed_codings("vaccineCode", [ - {"code": "999999", "system": "http://snomed.info/invalid", "display": "..."} - ]) + self._set_snomed_codings( + "vaccineCode", + [ + { + "code": "999999", + "system": "http://snomed.info/invalid", + "display": "...", + } + ], + ) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "") - + def test_vaccine_product_code_empty_coding_returns_empty(self): self._set_snomed_codings("vaccineCode", []) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "") - + def test_vaccine_product_code_single_coding_returns_first_code(self): - self._set_snomed_codings("vaccineCode", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "vaccineCode", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + } + ], + ) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "39114911000001101") - + def test_vaccine_product_code_double_coding_returns_first_code(self): - self._set_snomed_codings("vaccineCode", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "vaccineCode", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "39114911000001101") - def test_vaccine_product_code_double_coding_and_incorrect_system_returns_correct_code(self): - self._set_snomed_codings("vaccineCode", [ - {"code": "39114911000001101", "system": "http://snomed.info/invalid", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + def test_vaccine_product_code_double_coding_and_incorrect_system_returns_correct_code( + self, + ): + self._set_snomed_codings( + "vaccineCode", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/invalid", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_CODE, "39114911000001102") - + def test_site_vaccination_code_no_snomed_returns_empty(self): - self._set_snomed_codings("site", [ - {"code": "xyz", "system": "http://example.com/other"} - ]) - self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "") - + self._set_snomed_codings("site", [{"code": "xyz", "system": "http://example.com/other"}]) + self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "") + def test_site_vaccination_code_empty_coding_returns_empty(self): self._set_snomed_codings("site", []) self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "") - + def test_site_field_missing_returns_empty(self): self.request_json_data.pop("site", None) self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "") - + def test_site_vaccination_code_single_coding_returns_first_code(self): - self._set_snomed_codings("site", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "site", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + } + ], + ) self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "39114911000001101") def test_site_vaccination_code_double_coding_returns_first_code(self): - self._set_snomed_codings("site", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "site", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "39114911000001101") - def test_site_vaccination_code_double_coding_and_incorrect_system_returns_correct_code(self): - self._set_snomed_codings("site", [ - {"code": "39114911000001101", "system": "http://snomed.info/invalid", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + def test_site_vaccination_code_double_coding_and_incorrect_system_returns_correct_code( + self, + ): + self._set_snomed_codings( + "site", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/invalid", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_CODE, "39114911000001102") - + def test_route_vaccination_code_no_snomed_returns_empty(self): - self._set_snomed_codings("route", [ - {"code": "xyz", "system": "http://example.org"}, - {"code": "abc", "system": "http://example.net"} - ]) + self._set_snomed_codings( + "route", + [ + {"code": "xyz", "system": "http://example.org"}, + {"code": "abc", "system": "http://example.net"}, + ], + ) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "") - + def test_route_vaccination_code_empty_coding_returns_empty(self): self._set_snomed_codings("route", []) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "") - + def test_route_field_missing_returns_empty(self): self.request_json_data.pop("route", None) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "") - + def test_route_vaccination_code_single_coding_returns_first_code(self): - self._set_snomed_codings("route", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "route", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + } + ], + ) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "39114911000001101") def test_route_vaccination_code_double_coding_returns_first_code(self): - self._set_snomed_codings("route", [ - {"code": "39114911000001101", "system": "http://snomed.info/sct", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + self._set_snomed_codings( + "route", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/sct", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "39114911000001101") - def test_route_vaccination_code_double_coding_and_incorrect_system_returns_correct_code(self): - self._set_snomed_codings("route", [ - {"code": "39114911000001101", "system": "http://snomed.info/invalid", "display": "..."}, - {"code": "39114911000001102", "system": "http://snomed.info/sct", "display": "..."} - ]) + def test_route_vaccination_code_double_coding_and_incorrect_system_returns_correct_code( + self, + ): + self._set_snomed_codings( + "route", + [ + { + "code": "39114911000001101", + "system": "http://snomed.info/invalid", + "display": "...", + }, + { + "code": "39114911000001102", + "system": "http://snomed.info/sct", + "display": "...", + }, + ], + ) self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_CODE, "39114911000001102") - + def test_dose_unit_code_valid_snomed_returns_code(self): self.request_json_data["doseQuantity"] = { "code": "258684004", - "system": "http://snomed.info/sct" + "system": "http://snomed.info/sct", } self._run_snomed_test(ConversionFieldName.DOSE_UNIT_CODE, "258684004") def test_dose_unit_code_wrong_system_returns_empty(self): self.request_json_data["doseQuantity"] = { "code": "258684004", - "system": "http://unitsofmeasure.org" + "system": "http://unitsofmeasure.org", } self._run_snomed_test(ConversionFieldName.DOSE_UNIT_CODE, "") def test_dose_unit_code_missing_system_returns_empty(self): - self.request_json_data["doseQuantity"] = { - "code": "258684004" - } + self.request_json_data["doseQuantity"] = {"code": "258684004"} self._run_snomed_test(ConversionFieldName.DOSE_UNIT_CODE, "") def test_dose_unit_code_missing_code_returns_empty(self): - self.request_json_data["doseQuantity"] = { - "system": "http://snomed.info/sct" - } + self.request_json_data["doseQuantity"] = {"system": "http://snomed.info/sct"} self._run_snomed_test(ConversionFieldName.DOSE_UNIT_CODE, "") def test_dose_unit_code_missing_field_returns_empty(self): self.request_json_data.pop("doseQuantity", None) self._run_snomed_test(ConversionFieldName.DOSE_UNIT_CODE, "") - + def test_indication_code_single_reasoncode_with_valid_snomed(self): - self._set_snomed_codings("reasonCode", [ - {"system": "http://snomed.info/sct", "code": "123456"} - ]) + self._set_snomed_codings("reasonCode", [{"system": "http://snomed.info/sct", "code": "123456"}]) self._run_snomed_test(ConversionFieldName.INDICATION_CODE, "123456") def test_indication_code_multiple_reasoncodes_first_with_valid_snomed(self): self.request_json_data["reasonCode"] = [ {"coding": [{"system": "http://snomed.info/sct", "code": "111111"}]}, - {"coding": [{"system": "http://snomed.info/sct", "code": "222222"}]} + {"coding": [{"system": "http://snomed.info/sct", "code": "222222"}]}, ] self._run_snomed_test(ConversionFieldName.INDICATION_CODE, "111111") def test_indication_code_skips_invalid_system_and_selects_valid_next(self): self.request_json_data["reasonCode"] = [ {"coding": [{"system": "http://example.org", "code": "invalid"}]}, - {"coding": [{"system": "http://snomed.info/sct", "code": "999999"}]} + {"coding": [{"system": "http://snomed.info/sct", "code": "999999"}]}, ] self._run_snomed_test(ConversionFieldName.INDICATION_CODE, "999999") def test_indication_code_all_reasoncodes_invalid_system_returns_empty(self): self.request_json_data["reasonCode"] = [ {"coding": [{"system": "http://example.com", "code": "abc"}]}, - {"coding": [{"system": "http://example.org", "code": "def"}]} + {"coding": [{"system": "http://example.org", "code": "def"}]}, ] self._run_snomed_test(ConversionFieldName.INDICATION_CODE, "") diff --git a/delta_backend/tests/test_convert_snomed_terms.py b/delta_backend/tests/test_convert_snomed_terms.py index 5ee28049e..057e983f0 100644 --- a/delta_backend/tests/test_convert_snomed_terms.py +++ b/delta_backend/tests/test_convert_snomed_terms.py @@ -5,11 +5,12 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestSNOMEDTermsToFlatJson(unittest.TestCase): def setUp(self): self.request_json_data = copy.deepcopy(ValuesForTests.json_data) - + def _set_snomed_codings(self, target_path: str, codings: list[dict], extension_url: str = None): """Helper to insert coding entries into self.request_json_data at the desired FHIR path""" if target_path in {"vaccineCode", "site", "route"}: @@ -17,30 +18,26 @@ def _set_snomed_codings(self, target_path: str, codings: list[dict], extension_u elif target_path == "reasonCode": self.request_json_data["reasonCode"] = [{"coding": codings}] elif target_path == "extension": - self.request_json_data["extension"] = [{ - "url": extension_url or "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": codings + self.request_json_data["extension"] = [ + { + "url": extension_url + or "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": {"coding": codings}, } - }] + ] def _run_snomed_test(self, flat_field_name, expected_snomed_code): """Helper function to run the test""" self.converter = Converter(json.dumps(self.request_json_data)) flat_json = self.converter.run_conversion() self.assertEqual(flat_json.get(flat_field_name), expected_snomed_code) - + def test_vaccination_procedure_term_text_present(self): # Scenario 1: `text` field is present self._set_snomed_codings( target_path="extension", - codings=[ - { - "code": "dummy", - "system": "http://snomed.info/sct" - } - ], - extension_url="https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + codings=[{"code": "dummy", "system": "http://snomed.info/sct"}], + extension_url="https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", ) self.request_json_data["extension"][0]["valueCodeableConcept"]["text"] = "Procedure term from text" self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_TERM, "Procedure term from text") @@ -53,15 +50,20 @@ def test_vaccination_procedure_term_from_text(self): def test_vaccination_procedure_term_from_extension_value_string(self): """Test fallback to extension.valueString when text is missing.""" self.request_json_data["extension"][0]["valueCodeableConcept"].pop("text", None) # Remove text - self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_TERM, "Test Value string 123456 COVID19 vaccination") + self._run_snomed_test( + ConversionFieldName.VACCINATION_PROCEDURE_TERM, + "Test Value string 123456 COVID19 vaccination", + ) def test_vaccination_procedure_term_from_display_fallback(self): """Test fallback to display when no extension valueString is present.""" coding = self.request_json_data["extension"][0]["valueCodeableConcept"]["coding"][0] coding.pop("extension", None) # Remove all extensions self.request_json_data["extension"][0]["valueCodeableConcept"].pop("text", None) # Remove text - self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_TERM, - "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)") + self._run_snomed_test( + ConversionFieldName.VACCINATION_PROCEDURE_TERM, + "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", + ) def test_vaccination_procedure_term_null_when_nothing_matches(self): """Test null return when no text, no extension.valueString, and no display.""" @@ -76,29 +78,33 @@ def test_vaccination_procedure_term_skips_non_sct_systems(self): # Add a dummy non-SCT coding before the valid one self.request_json_data["extension"][0]["valueCodeableConcept"]["text"] = None # force to fallback path codings = self.request_json_data["extension"][0]["valueCodeableConcept"]["coding"] - codings.insert(0, { - "system": "http://not-snomed", - "code": "IGNORE", - "display": "Ignore this" - }) - self._run_snomed_test(ConversionFieldName.VACCINATION_PROCEDURE_TERM, "Test Value string 123456 COVID19 vaccination") - - + codings.insert( + 0, + {"system": "http://not-snomed", "code": "IGNORE", "display": "Ignore this"}, + ) + self._run_snomed_test( + ConversionFieldName.VACCINATION_PROCEDURE_TERM, + "Test Value string 123456 COVID19 vaccination", + ) + def test_vaccine_product_term_uses_first_snomed_value_string(self): - self._set_snomed_codings("vaccineCode", [ - { - "code": "123", - "system": "http://snomed.info/sct", - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Preferred display string" - } - ] - } - ]) + self._set_snomed_codings( + "vaccineCode", + [ + { + "code": "123", + "system": "http://snomed.info/sct", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Preferred display string", + } + ], + } + ], + ) self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_TERM, "Preferred display string") - + def test_vaccine_product_term_from_text(self): """Test when vaccineCode.text is present — it takes priority.""" self.request_json_data["vaccineCode"]["text"] = "Preferred vaccine product text" @@ -110,21 +116,24 @@ def test_vaccine_product_term_from_extension_value_string(self): # Modify first SNOMED coding sct_coding = next( - c for c in self.request_json_data["vaccineCode"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["vaccineCode"]["coding"] if c.get("system") == "http://snomed.info/sct" + ) + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Extension value from vaccine code", + } + ] + self._run_snomed_test( + ConversionFieldName.VACCINE_PRODUCT_TERM, + "Extension value from vaccine code", ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Extension value from vaccine code" - }] - self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_TERM, "Extension value from vaccine code") def test_vaccine_product_term_from_display_fallback(self): """Test fallback to display when text and extension.valueString are missing.""" self.request_json_data["vaccineCode"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["vaccineCode"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["vaccineCode"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding["display"] = "Display fallback for vaccine" @@ -134,8 +143,7 @@ def test_vaccine_product_term_returns_empty_when_no_data(self): """Test returns empty string when no text, no valueString, no display.""" self.request_json_data["vaccineCode"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["vaccineCode"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["vaccineCode"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding.pop("display", None) @@ -146,22 +154,26 @@ def test_vaccine_product_term_skips_non_sct_codings(self): self.request_json_data["vaccineCode"].pop("text", None) # Insert a non-SNOMED coding before SNOMED ones - self.request_json_data["vaccineCode"]["coding"].insert(0, { - "system": "http://not-snomed", - "code": "IGNORE", - "display": "Wrong system display" - }) + self.request_json_data["vaccineCode"]["coding"].insert( + 0, + { + "system": "http://not-snomed", + "code": "IGNORE", + "display": "Wrong system display", + }, + ) sct_coding = next( - c for c in self.request_json_data["vaccineCode"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["vaccineCode"]["coding"] if c.get("system") == "http://snomed.info/sct" ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Valid SNOMED vaccine product" - }] + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Valid SNOMED vaccine product", + } + ] self._run_snomed_test(ConversionFieldName.VACCINE_PRODUCT_TERM, "Valid SNOMED vaccine product") - + def test_site_of_vaccination_term_from_text(self): """Test when site.text is present — takes highest priority.""" self.request_json_data["site"]["text"] = "Left arm from text" @@ -172,21 +184,21 @@ def test_site_of_vaccination_term_from_extension_value_string(self): self.request_json_data["site"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["site"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["site"]["coding"] if c.get("system") == "http://snomed.info/sct" ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Extension site description" - }] + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Extension site description", + } + ] self._run_snomed_test(ConversionFieldName.SITE_OF_VACCINATION_TERM, "Extension site description") def test_site_of_vaccination_term_from_display(self): """Test fallback to display when text and extension are missing.""" self.request_json_data["site"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["site"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["site"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding["display"] = "Left upper arm (display)" @@ -196,8 +208,7 @@ def test_site_of_vaccination_term_returns_empty_when_no_valid_data(self): """Test when no text, no extension, no display.""" self.request_json_data["site"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["site"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["site"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding.pop("display", None) @@ -208,20 +219,24 @@ def test_site_of_vaccination_term_skips_non_sct_systems(self): self.request_json_data["site"].pop("text", None) # Add a non-SNOMED coding first - self.request_json_data["site"]["coding"].insert(0, { - "system": "http://not-snomed", - "code": "XYZ", - "display": "Invalid display" - }) + self.request_json_data["site"]["coding"].insert( + 0, + { + "system": "http://not-snomed", + "code": "XYZ", + "display": "Invalid display", + }, + ) sct_coding = next( - c for c in self.request_json_data["site"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["site"]["coding"] if c.get("system") == "http://snomed.info/sct" ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Valid SCT site term" - }] + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Valid SCT site term", + } + ] self._run_snomed_test("SITE_OF_VACCINATION_TERM", "Valid SCT site term") def test_route_of_vaccination_term_from_text(self): @@ -234,21 +249,24 @@ def test_route_of_vaccination_term_from_extension_value_string(self): self.request_json_data["route"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["route"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["route"]["coding"] if c.get("system") == "http://snomed.info/sct" + ) + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Intramuscular route from extension", + } + ] + self._run_snomed_test( + ConversionFieldName.ROUTE_OF_VACCINATION_TERM, + "Intramuscular route from extension", ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Intramuscular route from extension" - }] - self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_TERM, "Intramuscular route from extension") def test_route_of_vaccination_term_from_display(self): """Test fallback to display when text and extension are missing.""" self.request_json_data["route"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["route"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["route"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding["display"] = "Intranasal route" @@ -258,8 +276,7 @@ def test_route_of_vaccination_term_returns_empty_when_no_valid_data(self): """Test returns empty string when no text, extension, or display.""" self.request_json_data["route"].pop("text", None) sct_coding = next( - c for c in self.request_json_data["route"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["route"]["coding"] if c.get("system") == "http://snomed.info/sct" ) sct_coding.pop("extension", None) sct_coding.pop("display", None) @@ -270,20 +287,24 @@ def test_route_of_vaccination_term_skips_non_sct_systems(self): self.request_json_data["route"].pop("text", None) # Add a non-SNOMED coding first - self.request_json_data["route"]["coding"].insert(0, { - "system": "http://not-snomed", - "code": "999", - "display": "Invalid non-SCT display" - }) + self.request_json_data["route"]["coding"].insert( + 0, + { + "system": "http://not-snomed", + "code": "999", + "display": "Invalid non-SCT display", + }, + ) sct_coding = next( - c for c in self.request_json_data["route"]["coding"] - if c.get("system") == "http://snomed.info/sct" + c for c in self.request_json_data["route"]["coding"] if c.get("system") == "http://snomed.info/sct" ) - sct_coding["extension"] = [{ - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Correct route from SCT" - }] + sct_coding["extension"] = [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + "valueString": "Correct route from SCT", + } + ] self._run_snomed_test(ConversionFieldName.ROUTE_OF_VACCINATION_TERM, "Correct route from SCT") def test_dose_unit_term_when_unit_exists(self): @@ -292,7 +313,7 @@ def test_dose_unit_term_when_unit_exists(self): "value": 0.5, "unit": "milliliter", "system": "http://unitsofmeasure.org", - "code": "ml" + "code": "ml", } self._run_snomed_test(ConversionFieldName.DOSE_UNIT_TERM, "milliliter") @@ -306,7 +327,7 @@ def test_dose_unit_term_returns_empty_when_unit_missing(self): self.request_json_data["doseQuantity"] = { "value": 0.5, "system": "http://unitsofmeasure.org", - "code": "ml" + "code": "ml", # unit is missing } self._run_snomed_test(ConversionFieldName.DOSE_UNIT_TERM, "") diff --git a/delta_backend/tests/test_convert_unique_id.py b/delta_backend/tests/test_convert_unique_id.py index b0da9339e..277118108 100644 --- a/delta_backend/tests/test_convert_unique_id.py +++ b/delta_backend/tests/test_convert_unique_id.py @@ -5,6 +5,7 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestUniqueId(unittest.TestCase): def setUp(self): diff --git a/delta_backend/tests/test_convert_unique_id_uri.py b/delta_backend/tests/test_convert_unique_id_uri.py index 540b843f1..379087f2f 100644 --- a/delta_backend/tests/test_convert_unique_id_uri.py +++ b/delta_backend/tests/test_convert_unique_id_uri.py @@ -5,6 +5,7 @@ from converter import Converter from common.mappings import ConversionFieldName + class TestUniqueIdUri(unittest.TestCase): def setUp(self): diff --git a/delta_backend/tests/test_current_period.py b/delta_backend/tests/test_current_period.py index 27b628487..f4b75ec57 100644 --- a/delta_backend/tests/test_current_period.py +++ b/delta_backend/tests/test_current_period.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime, timezone -from extractor import Extractor +from extractor import Extractor class TestIsCurrentPeriod(unittest.TestCase): @@ -33,5 +33,6 @@ def test_no_period(self): result = self.extractor._is_current_period(name, self.occurrence) self.assertTrue(result) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/delta_backend/tests/test_delta.py b/delta_backend/tests/test_delta.py index ad78c669a..7add6fe1b 100644 --- a/delta_backend/tests/test_delta.py +++ b/delta_backend/tests/test_delta.py @@ -6,23 +6,25 @@ import decimal from common.mappings import EventName, Operation, ActionFlag from utils_for_converter_tests import ValuesForTests, RecordConfig +import delta +from delta import ( + send_message, + handler, + process_record, +) TEST_QUEUE_URL = "https://sqs.eu-west-2.amazonaws.com/123456789012/test-queue" - -# Set environment variables before importing the module -## @TODO: # Note: Environment variables shared across tests, thus aligned os.environ["AWS_SQS_QUEUE_URL"] = TEST_QUEUE_URL os.environ["DELTA_TABLE_NAME"] = "my_delta_table" os.environ["DELTA_TTL_DAYS"] = "14" os.environ["SOURCE"] = "my_source" -from delta import send_message, handler, process_record # Import after setting environment variables - SUCCESS_RESPONSE = {"ResponseMetadata": {"HTTPStatusCode": 200}} DUPLICATE_RESPONSE = ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "PutItem") EXCEPTION_RESPONSE = ClientError({"Error": {"Code": "InternalServerError"}}, "PutItem") FAIL_RESPONSE = {"ResponseMetadata": {"HTTPStatusCode": 500}} + class DeltaHandlerTestCase(unittest.TestCase): # TODO refactor for dependency injection, eg process_record, send_firehose etc @@ -45,7 +47,7 @@ def setUp(self): self.sqs_client_patcher = patch("delta.sqs_client") self.mock_sqs_client = self.sqs_client_patcher.start() - self.delta_table_patcher=patch("delta.delta_table") + self.delta_table_patcher = patch("delta.delta_table") self.mock_delta_table = self.delta_table_patcher.start() def tearDown(self): @@ -67,9 +69,7 @@ def test_send_message_success(self): send_message(record, sqs_queue_url) # Assert - self.mock_sqs_client.send_message.assert_called_once_with( - QueueUrl=sqs_queue_url, MessageBody=json.dumps(record) - ) + self.mock_sqs_client.send_message.assert_called_once_with(QueueUrl=sqs_queue_url, MessageBody=json.dumps(record)) def test_send_message_client_error(self): # Arrange @@ -91,7 +91,12 @@ def test_handler_success_insert(self): suppliers = ["RAVS", "EMIS"] for supplier in suppliers: imms_id = f"test-insert-imms-{supplier}-id" - event = ValuesForTests.get_event(event_name=EventName.CREATE, operation=Operation.CREATE, imms_id=imms_id, supplier=supplier) + event = ValuesForTests.get_event( + event_name=EventName.CREATE, + operation=Operation.CREATE, + imms_id=imms_id, + supplier=supplier, + ) # Act result = handler(event, None) @@ -99,8 +104,8 @@ def test_handler_success_insert(self): # Assert self.assertTrue(result) self.mock_delta_table.put_item.assert_called() - self.mock_firehose_logger.send_log.assert_called() # check logged - put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB + self.mock_firehose_logger.send_log.assert_called() # check logged + put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB put_item_data = put_item_call_args.kwargs["Item"] self.assertIn("Imms", put_item_data) self.assertEqual(put_item_data["Imms"]["ACTION_FLAG"], ActionFlag.CREATE) @@ -143,8 +148,8 @@ def test_handler_success_update(self): # Assert self.assertTrue(result) self.mock_delta_table.put_item.assert_called() - self.mock_firehose_logger.send_log.assert_called() # check logged - put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB + self.mock_firehose_logger.send_log.assert_called() # check logged + put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB put_item_data = put_item_call_args.kwargs["Item"] self.assertIn("Imms", put_item_data) self.assertEqual(put_item_data["Imms"]["ACTION_FLAG"], ActionFlag.UPDATE) @@ -156,7 +161,11 @@ def test_handler_success_delete_physical(self): # Arrange self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE imms_id = "test-update-imms-id" - event = ValuesForTests.get_event(event_name=EventName.DELETE_PHYSICAL, operation=Operation.DELETE_PHYSICAL, imms_id=imms_id) + event = ValuesForTests.get_event( + event_name=EventName.DELETE_PHYSICAL, + operation=Operation.DELETE_PHYSICAL, + imms_id=imms_id, + ) # Act result = handler(event, None) @@ -164,30 +173,32 @@ def test_handler_success_delete_physical(self): # Assert self.assertTrue(result) self.mock_delta_table.put_item.assert_called() - self.mock_firehose_logger.send_log.assert_called() # check logged - put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB + self.mock_firehose_logger.send_log.assert_called() # check logged + put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB put_item_data = put_item_call_args.kwargs["Item"] self.assertIn("Imms", put_item_data) self.assertEqual(put_item_data["Operation"], Operation.DELETE_PHYSICAL) self.assertEqual(put_item_data["ImmsID"], imms_id) - self.assertEqual(put_item_data["Imms"], "") # check imms has been blanked out + self.assertEqual(put_item_data["Imms"], "") # check imms has been blanked out self.mock_sqs_client.send_message.assert_not_called() def test_handler_success_delete_logical(self): # Arrange self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE imms_id = "test-update-imms-id" - event = ValuesForTests.get_event(event_name=EventName.UPDATE, - operation=Operation.DELETE_LOGICAL, - imms_id=imms_id) + event = ValuesForTests.get_event( + event_name=EventName.UPDATE, + operation=Operation.DELETE_LOGICAL, + imms_id=imms_id, + ) # Act result = handler(event, None) # Assert self.assertTrue(result) self.mock_delta_table.put_item.assert_called() - self.mock_firehose_logger.send_log.assert_called() # check logged - put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB + self.mock_firehose_logger.send_log.assert_called() # check logged + put_item_call_args = self.mock_delta_table.put_item.call_args # check data written to DynamoDB put_item_data = put_item_call_args.kwargs["Item"] self.assertIn("Imms", put_item_data) self.assertEqual(put_item_data["Imms"]["ACTION_FLAG"], ActionFlag.DELETE_LOGICAL) @@ -211,7 +222,7 @@ def test_dps_record_skipped(self, mock_logger_info): @patch("delta.Converter") def test_partial_success_with_errors(self, mock_converter): mock_converter_instance = MagicMock() - mock_converter_instance.run_conversion.return_value = {"ABC":"DEF"} + mock_converter_instance.run_conversion.return_value = {"ABC": "DEF"} mock_converter_instance.get_error_records.return_value = [{"error": "Invalid field"}] mock_converter.return_value = mock_converter_instance @@ -238,7 +249,7 @@ def test_partial_success_with_errors(self, mock_converter): # Assert the expected message is present self.assertIn( "Partial success: successfully synced into delta, but issues found within record", - status_desc + status_desc, ) def test_send_message_multi_records_diverse(self): @@ -247,7 +258,12 @@ def test_send_message_multi_records_diverse(self): records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "id1", ActionFlag.CREATE), RecordConfig(EventName.UPDATE, Operation.UPDATE, "id2", ActionFlag.UPDATE), - RecordConfig(EventName.DELETE_LOGICAL, Operation.DELETE_LOGICAL, "id3", ActionFlag.DELETE_LOGICAL), + RecordConfig( + EventName.DELETE_LOGICAL, + Operation.DELETE_LOGICAL, + "id3", + ActionFlag.DELETE_LOGICAL, + ), RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "id4"), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -261,13 +277,19 @@ def test_send_message_multi_records_diverse(self): self.assertEqual(self.mock_firehose_logger.send_log.call_count, len(records_config)) def test_send_message_skipped_records_diverse(self): - '''Check skipped records sent to firehose but not to DynamoDB''' + """Check skipped records sent to firehose but not to DynamoDB""" # Arrange self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "id1", ActionFlag.CREATE), RecordConfig(EventName.UPDATE, Operation.UPDATE, "id2", ActionFlag.UPDATE), - RecordConfig(EventName.CREATE, Operation.CREATE, "id-skip", ActionFlag.CREATE, "DPSFULL"), + RecordConfig( + EventName.CREATE, + Operation.CREATE, + "id-skip", + ActionFlag.CREATE, + "DPSFULL", + ), RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "id4"), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -286,7 +308,7 @@ def test_send_message_multi_create(self): records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "create-id1", ActionFlag.CREATE), RecordConfig(EventName.CREATE, Operation.CREATE, "create-id2", ActionFlag.CREATE), - RecordConfig(EventName.CREATE, Operation.CREATE, "create-id3", ActionFlag.CREATE) + RecordConfig(EventName.CREATE, Operation.CREATE, "create-id3", ActionFlag.CREATE), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -298,14 +320,13 @@ def test_send_message_multi_create(self): self.assertEqual(self.mock_delta_table.put_item.call_count, 3) self.assertEqual(self.mock_firehose_logger.send_log.call_count, 3) - def test_send_message_multi_update(self): # Arrange self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE records_config = [ RecordConfig(EventName.UPDATE, Operation.UPDATE, "update-id1", ActionFlag.UPDATE), RecordConfig(EventName.UPDATE, Operation.UPDATE, "update-id2", ActionFlag.UPDATE), - RecordConfig(EventName.UPDATE, Operation.UPDATE, "update-id3", ActionFlag.UPDATE) + RecordConfig(EventName.UPDATE, Operation.UPDATE, "update-id3", ActionFlag.UPDATE), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -322,9 +343,24 @@ def test_send_message_multi_logical_delete(self): self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE records_config = [ - RecordConfig(EventName.DELETE_LOGICAL, Operation.DELETE_LOGICAL, "delete-id1", ActionFlag.DELETE_LOGICAL), - RecordConfig(EventName.DELETE_LOGICAL, Operation.DELETE_LOGICAL, "delete-id2", ActionFlag.DELETE_LOGICAL), - RecordConfig(EventName.DELETE_LOGICAL, Operation.DELETE_LOGICAL, "delete-id3", ActionFlag.DELETE_LOGICAL) + RecordConfig( + EventName.DELETE_LOGICAL, + Operation.DELETE_LOGICAL, + "delete-id1", + ActionFlag.DELETE_LOGICAL, + ), + RecordConfig( + EventName.DELETE_LOGICAL, + Operation.DELETE_LOGICAL, + "delete-id2", + ActionFlag.DELETE_LOGICAL, + ), + RecordConfig( + EventName.DELETE_LOGICAL, + Operation.DELETE_LOGICAL, + "delete-id3", + ActionFlag.DELETE_LOGICAL, + ), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -342,7 +378,7 @@ def test_send_message_multi_physical_delete(self): records_config = [ RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "remove-id1"), RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "remove-id2"), - RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "remove-id3") + RecordConfig(EventName.DELETE_PHYSICAL, Operation.DELETE_PHYSICAL, "remove-id3"), ] event = ValuesForTests.get_multi_record_event(records_config) @@ -356,7 +392,11 @@ def test_send_message_multi_physical_delete(self): def test_single_error_in_multi(self): # Arrange - self.mock_delta_table.put_item.side_effect = [SUCCESS_RESPONSE, FAIL_RESPONSE, SUCCESS_RESPONSE] + self.mock_delta_table.put_item.side_effect = [ + SUCCESS_RESPONSE, + FAIL_RESPONSE, + SUCCESS_RESPONSE, + ] records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "ok-id1", ActionFlag.CREATE), @@ -377,7 +417,11 @@ def test_single_error_in_multi(self): def test_single_exception_in_multi(self): # Arrange # 2nd record fails - self.mock_delta_table.put_item.side_effect = [SUCCESS_RESPONSE, EXCEPTION_RESPONSE, SUCCESS_RESPONSE] + self.mock_delta_table.put_item.side_effect = [ + SUCCESS_RESPONSE, + EXCEPTION_RESPONSE, + SUCCESS_RESPONSE, + ] records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "ok-id2.1", ActionFlag.CREATE), @@ -396,7 +440,11 @@ def test_single_exception_in_multi(self): def test_single_duplicate_in_multi(self): # Arrange - self.mock_delta_table.put_item.side_effect = [SUCCESS_RESPONSE, DUPLICATE_RESPONSE, SUCCESS_RESPONSE] + self.mock_delta_table.put_item.side_effect = [ + SUCCESS_RESPONSE, + DUPLICATE_RESPONSE, + SUCCESS_RESPONSE, + ] records_config = [ RecordConfig(EventName.CREATE, Operation.CREATE, "ok-id2.1", ActionFlag.CREATE), @@ -417,13 +465,7 @@ def test_single_duplicate_in_multi(self): @patch("delta.send_firehose") def test_handler_calls_process_record_for_each_event(self, mock_send_firehose, mock_process_record): # Arrange - event = { - "Records": [ - { "a": "record1" }, - { "a": "record2" }, - { "a": "record3" } - ] - } + event = {"Records": [{"a": "record1"}, {"a": "record2"}, {"a": "record3"}]} # Mock process_record to always return True mock_process_record.return_value = True, {} mock_send_firehose.return_value = None @@ -442,13 +484,7 @@ def test_handler_sends_all_to_firehose(self, mock_send_firehose, mock_process_re # Arrange # event with 3 records - event = { - "Records": [ - { "a": "record1" }, - { "a": "record2" }, - { "a": "record3" } - ] - } + event = {"Records": [{"a": "record1"}, {"a": "record2"}, {"a": "record3"}]} return_ok = (True, {}) return_fail = (False, {}) mock_send_firehose.return_value = None @@ -463,6 +499,7 @@ def test_handler_sends_all_to_firehose(self, mock_send_firehose, mock_process_re # check that all records were sent to firehose self.assertEqual(mock_send_firehose.call_count, len(event["Records"])) + class DeltaRecordProcessorTestCase(unittest.TestCase): def setUp(self): @@ -478,7 +515,7 @@ def setUp(self): self.logger_exception_patcher = patch("logging.Logger.exception") self.mock_logger_exception = self.logger_exception_patcher.start() - self.delta_table_patcher=patch("delta.delta_table") + self.delta_table_patcher = patch("delta.delta_table") self.mock_delta_table = self.delta_table_patcher.start() def tearDown(self): @@ -522,8 +559,12 @@ def test_multi_record_success(self): def test_multi_record_success_with_fail(self): # Arrange - expected_returns = [ True, False, True] - self.mock_delta_table.put_item.side_effect = [SUCCESS_RESPONSE, FAIL_RESPONSE, SUCCESS_RESPONSE] + expected_returns = [True, False, True] + self.mock_delta_table.put_item.side_effect = [ + SUCCESS_RESPONSE, + FAIL_RESPONSE, + SUCCESS_RESPONSE, + ] test_configs = [ RecordConfig(EventName.CREATE, Operation.CREATE, "ok-id.1", ActionFlag.CREATE), RecordConfig(EventName.UPDATE, Operation.UPDATE, "fail-id.2", ActionFlag.UPDATE), @@ -542,12 +583,11 @@ def test_multi_record_success_with_fail(self): result, _ = process_record(record) # Assert - self.assertEqual(result, expected_returns[test_index-1]) + self.assertEqual(result, expected_returns[test_index - 1]) self.assertEqual(self.mock_delta_table.put_item.call_count, test_index) self.assertEqual(self.mock_logger_error.call_count, 1) - def test_single_record_table_exception(self): # Arrange @@ -575,11 +615,7 @@ def test_single_record_table_exception(self): def test_json_loads_called_with_parse_float_decimal(self, mock_json_loads): # Arrange - record = ValuesForTests.get_event_record( - imms_id="id", - event_name=EventName.UPDATE, - operation=Operation.UPDATE - ) + record = ValuesForTests.get_event_record(imms_id="id", event_name=EventName.UPDATE, operation=Operation.UPDATE) self.mock_delta_table.put_item.return_value = SUCCESS_RESPONSE # Act @@ -589,11 +625,9 @@ def test_json_loads_called_with_parse_float_decimal(self, mock_json_loads): mock_json_loads.assert_any_call(ValuesForTests.json_value_for_test, parse_float=decimal.Decimal) -import delta - class TestGetDeltaTable(unittest.TestCase): def setUp(self): - self.delta_table_patcher=patch("delta.delta_table") + self.delta_table_patcher = patch("delta.delta_table") self.mock_delta_table = self.delta_table_patcher.start() self.logger_info_patcher = patch("logging.Logger.info") self.mock_logger_info = self.logger_info_patcher.start() @@ -630,7 +664,6 @@ def test_returns_none_on_exception(self, mock_boto3_resource): self.assertIsNone(table) self.mock_logger_error.assert_called() -import delta class TestGetSqsClient(unittest.TestCase): def setUp(self): @@ -674,8 +707,6 @@ def test_returns_none_on_exception(self): self.mock_logger_error.assert_called() -import delta - class TestSendMessage(unittest.TestCase): def setUp(self): self.get_sqs_client_patcher = patch("delta.get_sqs_client") diff --git a/delta_backend/tests/test_log_firehose.py b/delta_backend/tests/test_log_firehose.py index 9ac716e07..6df02bf87 100644 --- a/delta_backend/tests/test_log_firehose.py +++ b/delta_backend/tests/test_log_firehose.py @@ -23,17 +23,17 @@ def test_send_log(self, mock_boto_client): mock_response = { "RecordId": "shardId-000000000000000000000001", "ResponseMetadata": { - "RequestId": "12345abcde67890fghijk", - "HTTPStatusCode": 200, - "RetryAttempts": 0 - } - } + "RequestId": "12345abcde67890fghijk", + "HTTPStatusCode": 200, + "RetryAttempts": 0, + }, + } mock_firehose_client = MagicMock() mock_boto_client.return_value = mock_firehose_client mock_firehose_client.put_record.return_value = mock_response - + stream_name = "stream_name" - firehose_logger = FirehoseLogger(boto_client=mock_firehose_client, stream_name=stream_name) + firehose_logger = FirehoseLogger(boto_client=mock_firehose_client, stream_name=stream_name) log_message = {"text": "Test log message"} # Act @@ -67,5 +67,6 @@ def test_send_log_failure(self, mock_boto_client): ) mock_logger_exception.assert_called_once_with("Error sending log to Firehose: Test exception") + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/delta_backend/tests/test_utils.py b/delta_backend/tests/test_utils.py index 134317d2b..0fcacb61a 100644 --- a/delta_backend/tests/test_utils.py +++ b/delta_backend/tests/test_utils.py @@ -1,6 +1,7 @@ import unittest from utils import is_valid_simple_snomed + class TestIsValidSimpleSnomed(unittest.TestCase): def test_valid_snomed(self): diff --git a/delta_backend/tests/utils_for_converter_tests.py b/delta_backend/tests/utils_for_converter_tests.py index 5de2d3916..6919650be 100644 --- a/delta_backend/tests/utils_for_converter_tests.py +++ b/delta_backend/tests/utils_for_converter_tests.py @@ -13,6 +13,7 @@ def __init__(self, event_name, operation, imms_id, expected_action_flag=None, su self.imms_id = imms_id self.expected_action_flag = expected_action_flag + class ValuesForTests: MOCK_ENVIRONMENT_DICT = { @@ -31,7 +32,12 @@ class ValuesForTests: { "resourceType": "Patient", "id": "Pat1", - "identifier": [{"system": "https://fhir.nhs.uk/Id/nhs-number", "value": "9000000009"}], + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + ], "name": [{"family": "Trailor", "given": ["Sam"]}], "gender": "unknown", "birthDate": "1965-02-28", @@ -50,19 +56,24 @@ class ValuesForTests: "extension": [ { "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", - "valueString": "Test Value string 123456 COVID19 vaccination" + "valueString": "Test Value string 123456 COVID19 vaccination", }, { "url": "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", - "valueId": "5306706018" - } + "valueId": "5306706018", + }, ], } ] }, } ], - "identifier": [{"system": "https://supplierABC/identifiers/vacc", "value": "ACME-vacc123456"}], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123456", + } + ], "status": "completed", "vaccineCode": { "coding": [ @@ -80,7 +91,10 @@ class ValuesForTests: "manufacturer": {"display": "AstraZeneca Ltd"}, "location": { "type": "Location", - "identifier": {"value": "EC1111", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}, + "identifier": { + "value": "EC1111", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + }, }, "lotNumber": "4120Z001", "expirationDate": "2021-07-02", @@ -113,7 +127,10 @@ class ValuesForTests: { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, } }, ], @@ -139,13 +156,14 @@ class ValuesForTests: json_value_for_test = json.dumps(json_data) @staticmethod - def get_event(event_name=EventName.CREATE, operation=Operation.CREATE, supplier="EMIS", imms_id="12345"): + def get_event( + event_name=EventName.CREATE, + operation=Operation.CREATE, + supplier="EMIS", + imms_id="12345", + ): """Create test event for the handler function.""" - return { - "Records": [ - ValuesForTests.get_event_record(imms_id, event_name, operation, supplier) - ] - } + return {"Records": [ValuesForTests.get_event_record(imms_id, event_name, operation, supplier)]} @staticmethod def get_multi_record_event(records_config: List[RecordConfig]): @@ -184,8 +202,8 @@ def get_event_record(imms_id, event_name, operation, supplier="EMIS"): "Operation": {"S": operation}, "SupplierSystem": {"S": supplier}, "Resource": {"S": ValuesForTests.json_value_for_test}, - } - } + }, + }, } else: return { @@ -198,8 +216,8 @@ def get_event_record(imms_id, event_name, operation, supplier="EMIS"): "PatientSK": {"S": "COVID19#ca8ba2c6-2383-4465-b456-c1174c21cf31"}, "SupplierSystem": {"S": supplier}, "Resource": {"S": ValuesForTests.json_value_for_test}, - } - } + }, + }, } expected_static_values = { @@ -213,82 +231,6 @@ def get_event_record(imms_id, event_name, operation, supplier="EMIS"): def get_expected_imms(expected_action_flag): """Returns expected Imms JSON data with the given action flag.""" return { - "NHS_NUMBER": "9000000009", - "PERSON_FORENAME": "Sam", - "PERSON_SURNAME": "Trailor", - "PERSON_DOB": "19650228", - "PERSON_GENDER_CODE": "0", - "PERSON_POSTCODE": "EC1A 1BB", - "DATE_AND_TIME": "20210207T13281700", - "SITE_CODE": "B0C4P", - "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "UNIQUE_ID": "ACME-vacc123456", - "UNIQUE_ID_URI": "https://supplierABC/identifiers/vacc", - "ACTION_FLAG": expected_action_flag, - "PERFORMING_PROFESSIONAL_FORENAME": "Florence", - "PERFORMING_PROFESSIONAL_SURNAME": "Nightingale", - "RECORDED_DATE": "20210207", - "PRIMARY_SOURCE": "TRUE", - "VACCINATION_PROCEDURE_CODE": "13246814444444", - "VACCINATION_PROCEDURE_TERM": "Test Value string 123456 COVID19 vaccination", - "DOSE_SEQUENCE": "1", - "VACCINE_PRODUCT_CODE": "39114911000001105", - "VACCINE_PRODUCT_TERM": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", - "VACCINE_MANUFACTURER": "AstraZeneca Ltd", - "BATCH_NUMBER": "4120Z001", - "EXPIRY_DATE": "20210702", - "SITE_OF_VACCINATION_CODE": "368208006", - "SITE_OF_VACCINATION_TERM": "Left upper arm structure (body structure)", - "ROUTE_OF_VACCINATION_CODE": "78421000", - "ROUTE_OF_VACCINATION_TERM": "Intramuscular route (qualifier value)", - "DOSE_AMOUNT": "0.5", - "DOSE_UNIT_CODE": "ml", - "DOSE_UNIT_TERM": "milliliter", - "INDICATION_CODE": "443684005", - "LOCATION_CODE": "EC1111", - "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "CONVERSION_ERRORS": [] - } - - expected_imms = { - "NHS_NUMBER": "9000000009", - "PERSON_FORENAME": "Sam", - "PERSON_SURNAME": "Trailor", - "PERSON_DOB": "19650228", - "PERSON_GENDER_CODE": "0", - "PERSON_POSTCODE": "EC1A 1BB", - "DATE_AND_TIME": "20210207T13281700", - "SITE_CODE": "B0C4P", - "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "UNIQUE_ID": "ACME-vacc123456", - "UNIQUE_ID_URI": "https://supplierABC/identifiers/vacc", - "ACTION_FLAG": "UPDATE", - "PERFORMING_PROFESSIONAL_FORENAME": "Florence", - "PERFORMING_PROFESSIONAL_SURNAME": "Nightingale", - "RECORDED_DATE": "20210207", - "PRIMARY_SOURCE": "TRUE", - "VACCINATION_PROCEDURE_CODE": "13246814444444", - "VACCINATION_PROCEDURE_TERM": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", - "DOSE_SEQUENCE": 1, - "VACCINE_PRODUCT_CODE": "39114911000001105", - "VACCINE_PRODUCT_TERM": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", - "VACCINE_MANUFACTURER": "AstraZeneca Ltd", - "BATCH_NUMBER": "4120Z001", - "EXPIRY_DATE": "20210702", - "SITE_OF_VACCINATION_CODE": "368208006", - "SITE_OF_VACCINATION_TERM": "Left upper arm structure (body structure)", - "ROUTE_OF_VACCINATION_CODE": "78421000", - "ROUTE_OF_VACCINATION_TERM": "Intramuscular route (qualifier value)", - "DOSE_AMOUNT": "0.5", - "DOSE_UNIT_CODE": "", - "DOSE_UNIT_TERM": "milliliter", - "INDICATION_CODE": "443684005", - "LOCATION_CODE": "EC1111", - "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "CONVERSION_ERRORS": [] - } - - expected_imms2 = { "NHS_NUMBER": "9000000009", "PERSON_FORENAME": "Sam", "PERSON_SURNAME": "Trailor", @@ -300,7 +242,7 @@ def get_expected_imms(expected_action_flag): "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", "UNIQUE_ID": "ACME-vacc123456", "UNIQUE_ID_URI": "https://supplierABC/identifiers/vacc", - "ACTION_FLAG": "UPDATE", + "ACTION_FLAG": expected_action_flag, "PERFORMING_PROFESSIONAL_FORENAME": "Florence", "PERFORMING_PROFESSIONAL_SURNAME": "Nightingale", "RECORDED_DATE": "20210207", @@ -323,9 +265,85 @@ def get_expected_imms(expected_action_flag): "INDICATION_CODE": "443684005", "LOCATION_CODE": "EC1111", "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "CONVERSION_ERRORS": [] + "CONVERSION_ERRORS": [], } + expected_imms = { + "NHS_NUMBER": "9000000009", + "PERSON_FORENAME": "Sam", + "PERSON_SURNAME": "Trailor", + "PERSON_DOB": "19650228", + "PERSON_GENDER_CODE": "0", + "PERSON_POSTCODE": "EC1A 1BB", + "DATE_AND_TIME": "20210207T13281700", + "SITE_CODE": "B0C4P", + "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", + "UNIQUE_ID": "ACME-vacc123456", + "UNIQUE_ID_URI": "https://supplierABC/identifiers/vacc", + "ACTION_FLAG": "UPDATE", + "PERFORMING_PROFESSIONAL_FORENAME": "Florence", + "PERFORMING_PROFESSIONAL_SURNAME": "Nightingale", + "RECORDED_DATE": "20210207", + "PRIMARY_SOURCE": "TRUE", + "VACCINATION_PROCEDURE_CODE": "13246814444444", + "VACCINATION_PROCEDURE_TERM": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)", + "DOSE_SEQUENCE": 1, + "VACCINE_PRODUCT_CODE": "39114911000001105", + "VACCINE_PRODUCT_TERM": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", + "VACCINE_MANUFACTURER": "AstraZeneca Ltd", + "BATCH_NUMBER": "4120Z001", + "EXPIRY_DATE": "20210702", + "SITE_OF_VACCINATION_CODE": "368208006", + "SITE_OF_VACCINATION_TERM": "Left upper arm structure (body structure)", + "ROUTE_OF_VACCINATION_CODE": "78421000", + "ROUTE_OF_VACCINATION_TERM": "Intramuscular route (qualifier value)", + "DOSE_AMOUNT": "0.5", + "DOSE_UNIT_CODE": "", + "DOSE_UNIT_TERM": "milliliter", + "INDICATION_CODE": "443684005", + "LOCATION_CODE": "EC1111", + "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", + "CONVERSION_ERRORS": [], + } + + expected_imms2 = { + "NHS_NUMBER": "9000000009", + "PERSON_FORENAME": "Sam", + "PERSON_SURNAME": "Trailor", + "PERSON_DOB": "19650228", + "PERSON_GENDER_CODE": "0", + "PERSON_POSTCODE": "EC1A 1BB", + "DATE_AND_TIME": "20210207T13281700", + "SITE_CODE": "B0C4P", + "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", + "UNIQUE_ID": "ACME-vacc123456", + "UNIQUE_ID_URI": "https://supplierABC/identifiers/vacc", + "ACTION_FLAG": "UPDATE", + "PERFORMING_PROFESSIONAL_FORENAME": "Florence", + "PERFORMING_PROFESSIONAL_SURNAME": "Nightingale", + "RECORDED_DATE": "20210207", + "PRIMARY_SOURCE": "TRUE", + "VACCINATION_PROCEDURE_CODE": "13246814444444", + "VACCINATION_PROCEDURE_TERM": "Test Value string 123456 COVID19 vaccination", + "DOSE_SEQUENCE": "1", + "VACCINE_PRODUCT_CODE": "39114911000001105", + "VACCINE_PRODUCT_TERM": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)", + "VACCINE_MANUFACTURER": "AstraZeneca Ltd", + "BATCH_NUMBER": "4120Z001", + "EXPIRY_DATE": "20210702", + "SITE_OF_VACCINATION_CODE": "368208006", + "SITE_OF_VACCINATION_TERM": "Left upper arm structure (body structure)", + "ROUTE_OF_VACCINATION_CODE": "78421000", + "ROUTE_OF_VACCINATION_TERM": "Intramuscular route (qualifier value)", + "DOSE_AMOUNT": "0.5", + "DOSE_UNIT_CODE": "ml", + "DOSE_UNIT_TERM": "milliliter", + "INDICATION_CODE": "443684005", + "LOCATION_CODE": "EC1111", + "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", + "CONVERSION_ERRORS": [], + } + class ErrorValuesForTests: @@ -361,7 +379,12 @@ class ErrorValuesForTests: }, } ], - "identifier": [{"system": "https://supplierABC/identifiers/vacc", "value": "ACME-vacc123456"}], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "ACME-vacc123456", + } + ], "status": "completed", "vaccineCode": { "coding": [ @@ -379,7 +402,10 @@ class ErrorValuesForTests: "manufacturer": {"display": "AstraZeneca Ltd"}, "location": { "type": "Location", - "identifier": {"value": "E712", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}, + "identifier": { + "value": "E712", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + }, }, "lotNumber": "4120Z001", "expirationDate": "2021-07-02", @@ -412,7 +438,10 @@ class ErrorValuesForTests: { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "B0C4P"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P", + }, } }, ], @@ -476,6 +505,6 @@ def get_expected_imms_error_output(expected_action_flag): "INDICATION_CODE": "443684005", "LOCATION_CODE": "E712", "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "CONVERSION_ERRORS": [] + "CONVERSION_ERRORS": [], } ] diff --git a/devtools/README.md b/devtools/README.md index 4fffad51d..0c78e979e 100644 --- a/devtools/README.md +++ b/devtools/README.md @@ -1,10 +1,13 @@ # Devtools - Localstack -## About +## About + LocalStack is a fully functional local cloud service emulator that allows developers to run AWS services locally without connecting to the actual AWS cloud. It is especially useful for testing, development, and CI/CD pipelines where real AWS resources are not needed or too costly. ## Setup: + 1. Install aws cli & awslocal (for localstack) for local testing of the infrastructure, might need to install unzip. AWSLocal is a wrapper for aws that simplifies interaction with LocalStack. + ``` curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip @@ -12,9 +15,10 @@ LocalStack is a fully functional local cloud service emulator that allows develo pip install awscli-local ``` + 2. Install terraform by following the instructions from `https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli` -3. Navigate to `devtools`. +3. Navigate to `devtools`. 4. Create a virtual environment in devtools: `python -m venv .venv`. 5. Activate virtual environment: `source .venv/bin/activate`. You should see a `(.venv)` as a prefix in your terminal. 6. Upgrade pip in the new environment: `pip install --upgrade pip`. @@ -22,7 +26,7 @@ LocalStack is a fully functional local cloud service emulator that allows develo 8. Run `make localstack` to setup localstack to run in docker. 9. Run `make init` or `terraform init`. If you get an error about failing to install providers, then remove the `.terraform.lock.hcl` and try again. 10. Run `make seed` to create a dynamodb table in localstack and add some data into it. -11. Run the following command to get a list of 10 items from dynamodb from localstack: +11. Run the following command to get a list of 10 items from dynamodb from localstack: ``` awslocal dynamodb query \ --table-name imms-default-imms-events \ @@ -32,11 +36,13 @@ LocalStack is a fully functional local cloud service emulator that allows develo 12. If you want to delete the table run `terraform apply -destroy` ## Interacting with localstack + The idea with localstack in regards to our project is to have a dynamodb table or an s3 bucket to interact with. We can't setup all the infrastructure on localstack because of the high complexity and lack of certain features such as AWS networking and IAM enforcement. 1. Check if localstack is running in docker by calling: `docker ps`. The output should display a running container with the image localstack/localstack 2. Ensure that you have a dynamodb table set up and some data inside the table provided by the `make seed` command. -3. You can install dynamodb-admin which is npm web app that connects to localstack, to set it up: +3. You can install dynamodb-admin which is npm web app that connects to localstack, to set it up: + ``` npm install -g dynamodb-admin @@ -48,7 +54,7 @@ The idea with localstack in regards to our project is to have a dynamodb table o # Navigate to the url provided, typically: http://localhost:8001/ where you should see the table imms-default-imms-events ``` -4. To interact with the code there are 2 options. You can either persist the application lambda via docker or you can directly run / debug the code directly in vscode. Please note some modifications are needed to configure the code to run successfuly. +4. To interact with the code there are 2 options. You can either persist the application lambda via docker or you can directly run / debug the code directly in vscode. Please note some modifications are needed to configure the code to run successfuly. 4.1 To run it via vs code, we can try the get_imms_handler, but first we should ensure that the request is correct so ensure that `event` has the folowing details. Note: that we are trying to retrieve the following immunisation record form the sample data `e3e70682-c209-4cac-629f-6fbed82c07cd` hence why we hardcoded the `VaccineTypePermissions`. @@ -63,13 +69,13 @@ The idea with localstack in regards to our project is to have a dynamodb table o }, } ``` + 4.2 If you want to run it via docker make the following changes in the lambda.Dockerfile: - - Set the dynamo db table name env variable to `imms-default-imms-events` + - Set the dynamo db table name env variable to `imms-default-imms-events` - Add `ENV DYNAMODB_TABLE_NAME=imms-default-imms-events` into the base section of the file - - Add the following line at the end in the lambda.Dockerfile: `CMD ["get_status_handler.get_status_handler"]` + - Add the following line at the end in the lambda.Dockerfile: `CMD ["get_status_handler.get_status_handler"]` - Test by sending a request via Postman to `http://localhost:8080/2015-03-31/functions/function/invocations` and add the event data into the body section. - ## Access Pattern We receive Immunisation Events in json format via our API. Each message contains inlined resources i.e. it doesn't @@ -77,19 +83,19 @@ contain a reference/link to a preexisting resource. For example, a vaccination e embedded in it. This means our backend doesn't assume Patient as a separate resource but rather, part of the message itself. This is the same for other resources included in the resource like address, location etc. -* **Creating an event:** Add the entire message in an attribute so, it can be retrieved. This attribute has the entire +- **Creating an event:** Add the entire message in an attribute so, it can be retrieved. This attribute has the entire original message with no changes. The event-id must be contained in the message itself. Our backend won't create the id. **This is our main index.** and it doesn't contain any sort-key -* **Retrieve an event:** The simplest form is by id; `GET /event/{id}`. This access pattern should always result in +- **Retrieve an event:** The simplest form is by id; `GET /event/{id}`. This access pattern should always result in either one resource or none -* **Search:** This pattern can be broken down into two main categories. Queries that retrieve events with a known +- **Search:** This pattern can be broken down into two main categories. Queries that retrieve events with a known patient and, queries that retrieve events with particular set of search criteria. ### Patient One index is dedicated to search patient. This will satisfy the `/event?NhsNumber=1234567,dateOfBirth=01/01/1970,diseaseTypeFilter=covid|flu|mmr|hpv|polio` endpoint. -The `NhsNumber` is our PK and the SK has `##` format. **This means, in order to +The `NhsNumber` is our PK and the SK has `##` format. \*\*This means, in order to filter based on `diseaseType`, `dateOfBirth` must be known. We can filter based on only `nhsNumber` and `diseaseType` but that requires an attribute filter. @@ -114,19 +120,20 @@ be similar to the patient access pattern. Given the relational model (below image) and `sample_event.json` below is our field mappings for highlighted fields: ![img](img/relational-model.png) -* `UNIQUE_ID -> $["identifier"][0]["value"]` -* `UNIQUE_ID_URI -> $["identifier"][0]["system"]` -* `ACTION_FLAG -> ?????` -* `VACCINATION_PROCEDURE_CODE -> $["extension"][0]["valueCodeableConcept"]["coding"][0]["code"]` -* `VACCINATION_PRODUCT_CODE -> $["vaccineCode"]["coding"][0]["code"]` -* `DISEASE_TYPE -> $["protocolApplied"][0]["targetDisease"][0]["coding"][0]["code"]` -* `LOCAL_PATIENT_ID -> $["contained"][0]["item"][3]["answer"][0]["valueCoding"]["code"]` -* `LOCAL_PATIENT_TYPE_URI -> $["contained"][0]["item"][3]["answer"][0]["valueCoding"]["system"]` +- `UNIQUE_ID -> $["identifier"][0]["value"]` +- `UNIQUE_ID_URI -> $["identifier"][0]["system"]` +- `ACTION_FLAG -> ?????` +- `VACCINATION_PROCEDURE_CODE -> $["extension"][0]["valueCodeableConcept"]["coding"][0]["code"]` +- `VACCINATION_PRODUCT_CODE -> $["vaccineCode"]["coding"][0]["code"]` +- `DISEASE_TYPE -> $["protocolApplied"][0]["targetDisease"][0]["coding"][0]["code"]` +- `LOCAL_PATIENT_ID -> $["contained"][0]["item"][3]["answer"][0]["valueCoding"]["code"]` +- `LOCAL_PATIENT_TYPE_URI -> $["contained"][0]["item"][3]["answer"][0]["valueCoding"]["system"]` TODO: write the rest of the mappings. **Q**: What does this mean: [[first point in Overview]](https://nhsd-confluence.digital.nhs.uk/display/Vacc/Immunisation+FHIR+API+-+IEDS+Data+Model) + > This data model must include backwards compatibility with the legacy CSV process to account for business continuity ## Error Scenarios @@ -137,7 +144,7 @@ Authentication/authorisation errors belong to this group. A second group is related to both fine and coarse validation errors. -* **Q:** What is expected in the response message? A detailed diagnostics of the validation or just a generic error? +- **Q:** What is expected in the response message? A detailed diagnostics of the validation or just a generic error? The third group is anything related to the backend itself. `404` and, catch-all exceptions in the source code (like malformed json for example) diff --git a/devtools/generate_data.py b/devtools/generate_data.py index f1e240b3e..25562aab0 100644 --- a/devtools/generate_data.py +++ b/devtools/generate_data.py @@ -20,15 +20,27 @@ def make_rand_id(): {"nhs_number": "1212233445"}, ] suppliers = [ - "https://supplierABC/ODSCode145", "https://supplierSDF/ODSCode123", "https://supplierXYZ/ODSCode735" + "https://supplierABC/ODSCode145", + "https://supplierSDF/ODSCode123", + "https://supplierXYZ/ODSCode735", ] disease_type = ["covid", "flu", "mmr", "hpv", "polio"] -vaccine_code = ["covidCode1", "covidCode2", "fluCode1", - "fluCode2", "fluCode3", - "mmrCode1", "mmrCode2", - "hpvCode1", - "polioCode1"] -vaccine_procedure = ["vac_procedure-oral", "vac_procedure-injection", "vac_procedure-123"] +vaccine_code = [ + "covidCode1", + "covidCode2", + "fluCode1", + "fluCode2", + "fluCode3", + "mmrCode1", + "mmrCode2", + "hpvCode1", + "polioCode1", +] +vaccine_procedure = [ + "vac_procedure-oral", + "vac_procedure-injection", + "vac_procedure-123", +] local_patient_pool = [ {"code": "ACME-Patient12345", "system": "https://supplierABC/identifiers/patient"}, {"code": "ACME-Patient23455", "system": "https://supplierCSB/identifiers/patient"}, @@ -76,6 +88,6 @@ def write(_data, resource_type): sample_file.write(json_events) -if __name__ == '__main__': +if __name__ == "__main__": imms = generate_immunization(30) write(imms, resource_type="immunization") diff --git a/devtools/localstack-compose.yml b/devtools/localstack-compose.yml index 25105f49b..5235e24e8 100644 --- a/devtools/localstack-compose.yml +++ b/devtools/localstack-compose.yml @@ -1,15 +1,15 @@ -version: '3.8' +version: "3.8" services: - localstack: - container_name: "localstack_main" - image: localstack/localstack:latest - environment: - - SERVICES=s3,dynamodb,sts - - DEFAULT_REGION=eu-west-2 - - DEBUG=1 - - PERSISTENCE=1 - ports: - - "4566:4566" - volumes: - - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" + localstack: + container_name: "localstack_main" + image: localstack/localstack:latest + environment: + - SERVICES=s3,dynamodb,sts + - DEFAULT_REGION=eu-west-2 + - DEBUG=1 + - PERSISTENCE=1 + ports: + - "4566:4566" + volumes: + - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" diff --git a/devtools/sample_event.json b/devtools/sample_event.json index 6a9cd0953..041b48ad2 100644 --- a/devtools/sample_event.json +++ b/devtools/sample_event.json @@ -199,9 +199,7 @@ "name": [ { "family": "test", - "given": [ - "test" - ] + "given": ["test"] } ], "gender": "1", @@ -296,9 +294,7 @@ "name": [ { "family": "test", - "given": [ - "test" - ] + "given": ["test"] } ] } diff --git a/devtools/seed.py b/devtools/seed.py index 94890045b..29c1899cf 100644 --- a/devtools/seed.py +++ b/devtools/seed.py @@ -11,7 +11,7 @@ class DynamoTable: def __init__(self, endpoint_url, _table_name): - db = boto3.resource('dynamodb', endpoint_url=endpoint_url, region_name="us-east-1") + db = boto3.resource("dynamodb", endpoint_url=endpoint_url, region_name="us-east-1") self.table = db.Table(_table_name) def create_immunization(self, immunization): @@ -22,13 +22,15 @@ def create_immunization(self, immunization): patient_sk = f"{disease_type}#{new_id}" - response = self.table.put_item(Item={ - 'PK': self._make_immunization_pk(new_id), - 'Resource': json.dumps(immunization), - 'PatientPK': self._make_patient_pk(patient_id), - 'PatientSK': patient_sk, - 'Version': 1 - }) + response = self.table.put_item( + Item={ + "PK": self._make_immunization_pk(new_id), + "Resource": json.dumps(immunization), + "PatientPK": self._make_patient_pk(patient_id), + "PatientSK": patient_sk, + "Version": 1, + } + ) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: return immunization @@ -54,7 +56,7 @@ def seed_immunization(table, _sample_file): print(f"{len(imms_list)} resources added successfully") -if __name__ == '__main__': +if __name__ == "__main__": _table = DynamoTable(dynamodb_url, table_name) seed_file = sample_file diff --git a/e2e/README.md b/e2e/README.md index d78357b1f..53334da0c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,5 +1,6 @@ ## End-to-end Tests -This directory contains end-to-end tests. Except for certain files, the majority of the tests hit the proxy (Apigee) and assert the response. + +This directory contains end-to-end tests. Except for certain files, the majority of the tests hit the proxy (Apigee) and assert the response. ## Setting up e2e tests to run locally @@ -8,6 +9,7 @@ This directory contains end-to-end tests. Except for certain files, the majority 2. Install the [get_token] utility provided by Apigee. It is required for authenticating with the Apigee platform. Please make sure you have an Apigee account for non-prod to be able to run these e2e tests. 3. Add the following values in the `.env` file and set the desired PR number. If there is already an `.env` file make sure that you only have the values specified below. + ``` APIGEE_USERNAME={your-apigee-email} APIGEE_ENVIRONMENT=internal-dev @@ -17,22 +19,22 @@ This directory contains end-to-end tests. Except for certain files, the majority There are other environment variables that are used, but these are the minimum amount for running tests locally. Apart from the first 4 items from the table below, you can ignore the rest of them. This will cause a few test failures, but it's safe to ignore them (locally). - | Name | Example | Description | - |--------------------|---------------------------------------------|-----------------------------------------------------------------------------------------------| - | `APIGEE_USERNAME` | your-nhs-email@nhs.net | Your NHS email address, used to authenticate with Apigee. | - | `APIGEE_ENVIRONMENT` | internal-dev | The Apigee environment you are targeting (e.g., `internal-dev`, `prod`, etc.). | - | `PROXY_NAME` | immunisation-fhir-api-pr-100 | The name of the Apigee proxy you want to target. You can find this in the Apigee UI. | - | `SERVICE_BASE_PATH` | immunisation-fhir-api/FHIR/R4-pr-100 | The base path for the proxy. This can be found in the "Overview" section of the Apigee UI. | + | Name | Example | Description | + | -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- | + | `APIGEE_USERNAME` | your-nhs-email@nhs.net | Your NHS email address, used to authenticate with Apigee. | + | `APIGEE_ENVIRONMENT` | internal-dev | The Apigee environment you are targeting (e.g., `internal-dev`, `prod`, etc.). | + | `PROXY_NAME` | immunisation-fhir-api-pr-100 | The name of the Apigee proxy you want to target. You can find this in the Apigee UI. | + | `SERVICE_BASE_PATH` | immunisation-fhir-api/FHIR/R4-pr-100 | The base path for the proxy. This can be found in the "Overview" section of the Apigee UI. | | `STATUS_API_KEY` | secret | Used to test the `_status` endpoint. If not set, that specific test will fail (can be ignored locally). | - | `AWS_PROFILE` | apim-dev | Some operations require the AWS CLI. This profile is used for AWS authentication. | - | `AWS_DOMAIN_NAME` | https://pr-100.imms.dev.vds.platform.nhs.uk | The domain pointing to the backend deployment, used for testing mTLS. Can be ignored locally. | - + | `AWS_PROFILE` | apim-dev | Some operations require the AWS CLI. This profile is used for AWS authentication. | + | `AWS_DOMAIN_NAME` | https://pr-100.imms.dev.vds.platform.nhs.uk | The domain pointing to the backend deployment, used for testing mTLS. Can be ignored locally. | 4. If you prefer to skip Terraform initialization, you can configure the `Makefile` to work without it. This step is optional — if Terraform has already been initialized, you can proceed to the next step. **Note:** The `Makefile` includes environment variables that are dynamically set when you run its commands. These variables are populated by scripts that retrieve information about AWS resources deployed to the specified environment. For local development, you can simplify the setup by modifying the configuration to point directly to resource names, instead of relying on Terraform initialization. + ``` APIGEE_ACCESS_TOKEN ?= $(shell export SSO_LOGIN_URL=https://login.apigee.com && eval get_token -u $(APIGEE_USERNAME)) AWS_DOMAIN_NAME="" @@ -71,10 +73,10 @@ that way. The content of this directory can be broken down into three categories: -* **apigee:** Everything you need to set up Apigee app, product and authentication -* **authentication:** Contains everything you need to perform proxy authentication. It covers all three types of +- **apigee:** Everything you need to set up Apigee app, product and authentication +- **authentication:** Contains everything you need to perform proxy authentication. It covers all three types of authentication -* **env:** The utilities in the `lib` directory never assumes configurations. You need to pass them directly to create +- **env:** The utilities in the `lib` directory never assumes configurations. You need to pass them directly to create an instance of the required tool. The `env.py` file is to make assumptions about the source of the configuration. When making changes to this file keep two things in mind. 1- reduce the amount of config by convention over configuration @@ -86,4 +88,3 @@ The files in this directory are test utilities, but they are still project agnos anything about your particular project. Think of this directory more like a higher level wrapper around `lib`. The most important file in this directory is the `base_test.py` file, which contains the test setup and teardown logic related to common e2e tests. - diff --git a/e2e/lib/apigee.py b/e2e/lib/apigee.py index f67fd3717..ba3f757cd 100644 --- a/e2e/lib/apigee.py +++ b/e2e/lib/apigee.py @@ -39,7 +39,8 @@ def base_url(self): @dataclass class ApigeeApp: - """ Data object to create an apigee app or decode json response""" + """Data object to create an apigee app or decode json response""" + name: str appId: str = None credentials: List[dict] = field(default_factory=lambda: []) @@ -74,15 +75,13 @@ def dict(self): @classmethod def from_dict(cls, data): # Only consider keys that are in the class definition - return cls(**{ - k: v for k, v in data.items() - if k in inspect.signature(cls).parameters - }) + return cls(**{k: v for k, v in data.items() if k in inspect.signature(cls).parameters}) @dataclass class ApigeeProduct: """Data object to create an apigee product""" + name: str apiResources: List[str] = field(default_factory=lambda: []) approvalType: str = "auto" @@ -103,10 +102,7 @@ def dict(self): @classmethod def from_dict(cls, data): # Only consider keys that are in the class definition - return cls(**{ - k: v for k, v in data.items() - if k in inspect.signature(cls).parameters - }) + return cls(**{k: v for k, v in data.items() if k in inspect.signature(cls).parameters}) class ApigeeService: @@ -166,7 +162,8 @@ def _get(self, path: str, params: dict = None) -> dict: if resp.status_code != 200: raise ApigeeError( f"GET request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}") + f"Reason: {resp.reason} and Content: {resp.text}" + ) return resp.json() def _create(self, path: str, body: dict) -> dict: @@ -175,7 +172,8 @@ def _create(self, path: str, body: dict) -> dict: if resp.status_code != 200 and resp.status_code != 201: raise ApigeeError( f"POST request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}") + f"Reason: {resp.reason} and Content: {resp.text}" + ) return resp.json() def _update(self, path: str, body: dict) -> dict: @@ -184,7 +182,8 @@ def _update(self, path: str, body: dict) -> dict: if resp.status_code != 200: raise ApigeeError( f"PUT request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}") + f"Reason: {resp.reason} and Content: {resp.text}" + ) return resp.json() def _delete(self, path: str) -> dict: @@ -194,5 +193,6 @@ def _delete(self, path: str) -> dict: if resp.status_code != 200 and resp.status_code != 404: raise ApigeeError( f"DELETE request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}") + f"Reason: {resp.reason} and Content: {resp.text}" + ) return resp.json() diff --git a/e2e/lib/authentication.py b/e2e/lib/authentication.py index 811ea9ea4..9400ebcab 100644 --- a/e2e/lib/authentication.py +++ b/e2e/lib/authentication.py @@ -69,22 +69,22 @@ def get_access_token(self): "aud": self.token_url, "iat": now, "exp": now + self.expiry, - "jti": str(uuid.uuid4()) + "jti": str(uuid.uuid4()), } - _jwt = jwt.encode(claims, self.private_key, algorithm='RS512', headers={"kid": self.kid}) + _jwt = jwt.encode(claims, self.private_key, algorithm="RS512", headers={"kid": self.kid}) - headers = {'Content-Type': 'application/x-www-form-urlencoded'} + headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { - 'grant_type': 'client_credentials', - 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - 'client_assertion': _jwt + "grant_type": "client_credentials", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": _jwt, } token_response = requests.post(self.token_url, data=data, headers=headers) if token_response.status_code != 200: raise AuthenticationError(f"ApplicationRestricted token POST request failed\n{token_response.text}") - return token_response.json().get('access_token') + return token_response.json().get("access_token") class AuthCodeFlow: diff --git a/e2e/lib/env.py b/e2e/lib/env.py index 45c4f9edc..d9df56146 100644 --- a/e2e/lib/env.py +++ b/e2e/lib/env.py @@ -22,8 +22,10 @@ def get_apigee_env() -> ApigeeEnv: except ValueError: logging.error(f'the environment variable "APIGEE_ENVIRONMENT: {env}" is invalid') else: - logging.warning('the environment variable "APIGEE_ENVIRONMENT" is empty, ' - 'falling back to the default value: "internal-dev"') + logging.warning( + 'the environment variable "APIGEE_ENVIRONMENT" is empty, ' + 'falling back to the default value: "internal-dev"' + ) return ApigeeEnv.INTERNAL_DEV @@ -35,11 +37,18 @@ def get_apigee_access_token(username: str = None): env = os.environ.copy() env["SSO_LOGIN_URL"] = env.get("SSO_LOGIN_URL", "https://login.apigee.com") try: - res = subprocess.run(["get_token", "-u", username], env=env, stdout=subprocess.PIPE, text=True) + res = subprocess.run( + ["get_token", "-u", username], + env=env, + stdout=subprocess.PIPE, + text=True, + ) return res.stdout.strip() except FileNotFoundError: - raise RuntimeError("Make sure you install apigee's get_token utility and make sure it's in your PATH. " - "Follow: https://docs.apigee.com/api-platform/system-administration/using-gettoken") + raise RuntimeError( + "Make sure you install apigee's get_token utility and make sure it's in your PATH. " + "Follow: https://docs.apigee.com/api-platform/system-administration/using-gettoken" + ) def get_default_app_restricted_credentials() -> AppRestrictedCredentials: @@ -92,7 +101,7 @@ def get_service_base_path(apigee_env: ApigeeEnv = None) -> str: apigee_env = apigee_env if apigee_env else get_apigee_env() base_path = os.getenv("SERVICE_BASE_PATH") - if apigee_env.value == 'prod': + if apigee_env.value == "prod": return f"https://api.service.nhs.uk/{base_path}" return f"https://{apigee_env.value}.api.service.nhs.uk/{base_path}" diff --git a/e2e/lib/jwks.py b/e2e/lib/jwks.py index 651f6df7f..c8c169340 100644 --- a/e2e/lib/jwks.py +++ b/e2e/lib/jwks.py @@ -29,8 +29,8 @@ def __init__(self, key_id: str, private_key_path: str = None, public_key_path: s pub_key = serialization.load_pem_public_key(self.public_key.encode(), backend=default_backend()) n = pub_key.public_numbers().n - n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder='big') - self.encoded_n = base64.urlsafe_b64encode(n_bytes).decode('utf-8') + n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder="big") + self.encoded_n = base64.urlsafe_b64encode(n_bytes).decode("utf-8") def get_jwk(self): return { @@ -38,7 +38,8 @@ def get_jwk(self): "n": self.encoded_n, "e": "AQAB", "alg": "RS512", - "kid": self.key_id} + "kid": self.key_id, + } def get_jwks_url(self, base_url: str) -> str: jwks = json.dumps({"keys": [self.get_jwk()]}) @@ -52,21 +53,20 @@ def _make_key_pair_n(key_size=4096) -> (str, str, str): prv = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption()) + encryption_algorithm=serialization.NoEncryption(), + ) public_key = private_key.public_key() - pub = public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.PKCS1) + pub = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.PKCS1) n = public_key.public_numbers().n - n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder='big') - n_encoded = base64.urlsafe_b64encode(n_bytes).decode('utf-8') + n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder="big") + n_encoded = base64.urlsafe_b64encode(n_bytes).decode("utf-8") return prv.decode(), pub.decode(), n_encoded -if __name__ == '__main__': +if __name__ == "__main__": p = "/Users/jalal/tmp/imms-batch-key" kid = "ecf6452b-96a5-44b9-95cd-6dae700e85a8" pk = f"{p}/imms-batch.key" diff --git a/e2e/test_authorization.py b/e2e/test_authorization.py index c200d43ac..1b112568f 100644 --- a/e2e/test_authorization.py +++ b/e2e/test_authorization.py @@ -1,5 +1,5 @@ -import uuid import unittest +import uuid from typing import Set from lib.apigee import ApigeeApp @@ -14,8 +14,8 @@ from utils.constants import valid_nhs_number1, cis2_user from utils.factories import make_app_restricted_app, make_cis2_app from utils.immunisation_api import ImmunisationApi -from utils.resource import generate_imms_resource from utils.mappings import VaccineTypes +from utils.resource import generate_imms_resource @unittest.skip("Skipping this entire test suite for now") diff --git a/e2e/test_create_immunization.py b/e2e/test_create_immunization.py index 29c69245f..10d592b4f 100644 --- a/e2e/test_create_immunization.py +++ b/e2e/test_create_immunization.py @@ -35,7 +35,10 @@ def test_non_unique_identifier(self): self.assertEqual(res.status_code, 200) # Check that duplicate CREATE request is rejected - self.assert_operation_outcome(self.default_imms_api.create_immunization(imms, expected_status_code=422), 422) + self.assert_operation_outcome( + self.default_imms_api.create_immunization(imms, expected_status_code=422), + 422, + ) self.assertEqual(res.headers["E-Tag"], "1") # Check that duplicate CREATE request is rejected after the event is updated @@ -43,13 +46,21 @@ def test_non_unique_identifier(self): self.default_imms_api.update_immunization(imms_id, imms) self.assertEqual(res.status_code, 200) del imms["id"] # Imms fhir resource should not include an id for create - self.assert_operation_outcome(self.default_imms_api.create_immunization(imms, expected_status_code=422), 422) + self.assert_operation_outcome( + self.default_imms_api.create_immunization(imms, expected_status_code=422), + 422, + ) # Check that duplicate CREATE request is rejected after the event is updated then deleted self.default_imms_api.delete_immunization(imms_id) - self.assertEqual(self.default_imms_api.get_immunization_by_id( - imms_id, expected_status_code=404).status_code, 404) - self.assert_operation_outcome(self.default_imms_api.create_immunization(imms, expected_status_code=422), 422) + self.assertEqual( + self.default_imms_api.get_immunization_by_id(imms_id, expected_status_code=404).status_code, + 404, + ) + self.assert_operation_outcome( + self.default_imms_api.create_immunization(imms, expected_status_code=422), + 422, + ) # Check that duplicate CREATE request is rejected after the event is updated then deleted then reinstated imms["id"] = imms_id # Imms fhir resource should include the id for update @@ -57,7 +68,10 @@ def test_non_unique_identifier(self): res = self.default_imms_api.get_immunization_by_id(imms_id) self.assertEqual(res.status_code, 200) del imms["id"] # Imms fhir resource should not include an id for create - self.assert_operation_outcome(self.default_imms_api.create_immunization(imms, expected_status_code=422), 422) + self.assert_operation_outcome( + self.default_imms_api.create_immunization(imms, expected_status_code=422), + 422, + ) self.assertEqual(res.headers["E-Tag"], "3") def test_invalid_nhs_number(self): @@ -116,7 +130,8 @@ def test_no_patient_identifier(self): def test_create_imms_for_mandatory_fields_only(self): """Test that data containing only the mandatory fields is accepted for create""" imms = generate_imms_resource( - nhs_number=None, sample_data_file_name="completed_covid19_immunization_event_mandatory_fields_only" + nhs_number=None, + sample_data_file_name="completed_covid19_immunization_event_mandatory_fields_only", ) # When @@ -130,7 +145,8 @@ def test_create_imms_for_mandatory_fields_only(self): def test_create_imms_with_missing_mandatory_field(self): """Test that data is rejected for create if one of the mandatory fields is missing""" imms = generate_imms_resource( - nhs_number=None, sample_data_file_name="completed_covid19_immunization_event_mandatory_fields_only" + nhs_number=None, + sample_data_file_name="completed_covid19_immunization_event_mandatory_fields_only", ) del imms["primarySource"] diff --git a/e2e/test_delete_immunization.py b/e2e/test_delete_immunization.py index 0fc9430d9..ffcd79106 100644 --- a/e2e/test_delete_immunization.py +++ b/e2e/test_delete_immunization.py @@ -12,7 +12,7 @@ def test_delete_imms(self): # Given immunization_data_list = [ generate_imms_resource(), - generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event") + generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event"), ] created_ids = [] diff --git a/e2e/test_delta_immunization.py b/e2e/test_delta_immunization.py index 1e5324788..87689a3a1 100644 --- a/e2e/test_delta_immunization.py +++ b/e2e/test_delta_immunization.py @@ -1,10 +1,11 @@ +import copy +import os +import time from datetime import datetime + from utils.base_test import ImmunizationBaseTest from utils.immunisation_api import parse_location from utils.resource import generate_imms_resource, get_dynamodb_table -import time -import copy -import os class TestDeltaImmunization(ImmunizationBaseTest): diff --git a/e2e/test_deployment.py b/e2e/test_deployment.py index c53810438..88b03c469 100644 --- a/e2e/test_deployment.py +++ b/e2e/test_deployment.py @@ -3,7 +3,11 @@ import requests -from lib.env import get_service_base_path, get_status_endpoint_api_key, get_source_commit_id +from lib.env import ( + get_service_base_path, + get_status_endpoint_api_key, + get_source_commit_id, +) """Tests in this package don't really test anything. Platform created these tests to check if the current deployment is the latest. It works by hitting /_status endpoint and comparing the commit sha code of the @@ -33,7 +37,7 @@ def test_wait_for_status(self): self.check_and_retry(url, {"apikey": self.status_api_key}, self.expected_commit_id) def check_and_retry(self, url, headers, expected_commit_id): - for i in range(self.max_retries): + for _i in range(self.max_retries): resp = requests.get(url, headers=headers) status_code = resp.status_code if status_code != 200: diff --git a/e2e/test_get_immunization.py b/e2e/test_get_immunization.py index 7aa255d9e..0d8e982c3 100644 --- a/e2e/test_get_immunization.py +++ b/e2e/test_get_immunization.py @@ -1,10 +1,10 @@ -from decimal import Decimal import uuid +from decimal import Decimal from utils.base_test import ImmunizationBaseTest from utils.immunisation_api import parse_location -from utils.resource import generate_imms_resource, generate_filtered_imms_resource from utils.mappings import EndpointOperationNames, VaccineTypes +from utils.resource import generate_imms_resource, generate_filtered_imms_resource class TestGetImmunization(ImmunizationBaseTest): @@ -22,19 +22,21 @@ def test_get_imms(self): "data": generate_imms_resource(imms_identifier_value=covid_uuid), "expected": generate_filtered_imms_resource( crud_operation_to_filter_for=EndpointOperationNames.READ, - imms_identifier_value=covid_uuid) + imms_identifier_value=covid_uuid, + ), }, { "data": generate_imms_resource( sample_data_file_name="completed_rsv_immunization_event", vaccine_type=VaccineTypes.rsv, - imms_identifier_value=rsv_uuid), + imms_identifier_value=rsv_uuid, + ), "expected": generate_filtered_imms_resource( crud_operation_to_filter_for=EndpointOperationNames.READ, vaccine_type=VaccineTypes.rsv, - imms_identifier_value=rsv_uuid - ) - } + imms_identifier_value=rsv_uuid, + ), + }, ] # Create immunizations and capture IDs diff --git a/e2e/test_proxy.py b/e2e/test_proxy.py index 098da9034..ead3f24b0 100644 --- a/e2e/test_proxy.py +++ b/e2e/test_proxy.py @@ -2,9 +2,11 @@ import subprocess import unittest import uuid + import requests -from utils.immunisation_api import ImmunisationApi + from lib.env import get_service_base_path, get_status_endpoint_api_key +from utils.immunisation_api import ImmunisationApi class TestProxyHealthcheck(unittest.TestCase): @@ -24,15 +26,20 @@ def test_ping(self): def test_status(self): """/_status should return 200 if proxy can reach to the backend""" - response = ImmunisationApi.make_request_with_backoff(http_method="GET", - url=f"{self.proxy_url}/_status", - headers={"apikey": self.status_api_key}, - is_status_check=True) + response = ImmunisationApi.make_request_with_backoff( + http_method="GET", + url=f"{self.proxy_url}/_status", + headers={"apikey": self.status_api_key}, + is_status_check=True, + ) self.assertEqual(response.status_code, 200, response.text) body = response.json() - self.assertEqual(body["status"].lower(), "pass", - f"service is not healthy: status: {body['status']}") + self.assertEqual( + body["status"].lower(), + "pass", + f"service is not healthy: status: {body['status']}", + ) class TestMtls(unittest.TestCase): @@ -47,13 +54,15 @@ def test_mtls(self): ImmunisationApi.make_request_with_backoff( http_method="GET", url=backend_health, - headers={"X-Request-ID": str(uuid.uuid4())}) + headers={"X-Request-ID": str(uuid.uuid4())}, + ) self.assertTrue("RemoteDisconnected" in str(e.exception)) @staticmethod def get_backend_url() -> str: """The output is the backend url that terraform has deployed. - This command runs a make target in the terraform directory only if it's not in env var""" + This command runs a make target in the terraform directory only if it's not in env var + """ if url := os.getenv("AWS_DOMAIN_NAME"): return url @@ -66,18 +75,22 @@ def get_backend_url() -> str: cmd_str = " ".join(cmd) raise RuntimeError( f"Failed to run command: '{cmd_str}'\nDiagnostic: Try to run the same command in the " - f"same terminal. Make sure you are authenticated\n{res.stdout}") + f"same terminal. Make sure you are authenticated\n{res.stdout}" + ) return res.stdout except FileNotFoundError: - raise RuntimeError("Make sure you install terraform. This test can only be run if you have access to the" - "backend deployment") + raise RuntimeError( + "Make sure you install terraform. This test can only be run if you have access to the" + "backend deployment" + ) except RuntimeError as e: raise RuntimeError(f"Failed to run command\n{e}") class TestProxyAuthorization(unittest.TestCase): """Our apigee proxy has its own authorization. - This class test different authorization access levels/roles authentication types that are supported""" + This class test different authorization access levels/roles authentication types that are supported + """ proxy_url: str @@ -87,8 +100,10 @@ def setUpClass(cls): def test_invalid_access_token(self): """it should return 401 if access token is invalid""" - response = ImmunisationApi.make_request_with_backoff(http_method="GET", - url=f"{self.proxy_url}/Immunization", - headers={"X-Request-ID": str(uuid.uuid4())}, - expected_status_code=401) + response = ImmunisationApi.make_request_with_backoff( + http_method="GET", + url=f"{self.proxy_url}/Immunization", + headers={"X-Request-ID": str(uuid.uuid4())}, + expected_status_code=401, + ) self.assertEqual(response.status_code, 401, response.text) diff --git a/e2e/test_search_by_identifier_immunization.py b/e2e/test_search_by_identifier_immunization.py index 0ca9154ec..fda427b3c 100644 --- a/e2e/test_search_by_identifier_immunization.py +++ b/e2e/test_search_by_identifier_immunization.py @@ -1,14 +1,13 @@ - -from decimal import Decimal import pprint -from typing import NamedTuple, Literal, Optional import uuid +from decimal import Decimal +from typing import NamedTuple, Literal, Optional + +from lib.env import get_service_base_path from utils.base_test import ImmunizationBaseTest from utils.constants import valid_nhs_number1 - -from utils.resource import generate_imms_resource, generate_filtered_imms_resource from utils.mappings import VaccineTypes -from lib.env import get_service_base_path +from utils.resource import generate_imms_resource, generate_filtered_imms_resource class TestSearchImmunizationByIdentifier(ImmunizationBaseTest): @@ -53,9 +52,8 @@ def test_search_imms(self): # When rsv_search_response = imms_api.search_immunization_by_identifier( - rsv_identifier_system, - rsv_identifier_value - ) + rsv_identifier_system, rsv_identifier_value + ) self.assertEqual(rsv_search_response.status_code, 200, search_response.text) rsv_bundle = rsv_search_response.json() self.assertEqual(bundle.get("resourceType"), "Bundle", rsv_bundle) @@ -122,14 +120,19 @@ def test_search_backwards_compatible(self): self.assertTrue(entry["fullUrl"].startswith("https://")) self.assertEqual(entry["resource"]["resourceType"], "Immunization") imms_identifier = entry["resource"]["identifier"] - self.assertEqual(len(imms_identifier), 1, "Immunization did not have exactly 1 identifier") + self.assertEqual( + len(imms_identifier), + 1, + "Immunization did not have exactly 1 identifier", + ) self.assertEqual(imms_identifier[0]["system"], identifier_system) self.assertEqual(imms_identifier[0]["value"], identifier_value) # Check structure of one of the imms resources response_imm = next(item for item in entries if item["resource"]["id"] == imms_id) self.assertEqual( - response_imm["fullUrl"], f"{get_service_base_path()}/Immunization/{imms_id}" + response_imm["fullUrl"], + f"{get_service_base_path()}/Immunization/{imms_id}", ) self.assertEqual(response_imm["search"], {"mode": "match"}) expected_imms_resource["patient"]["reference"] = response_imm["resource"]["patient"]["reference"] @@ -137,8 +140,10 @@ def test_search_backwards_compatible(self): def test_search_immunization_parameter_smoke_tests(self): stored_records = generate_imms_resource( - valid_nhs_number1, VaccineTypes.covid_19, - imms_identifier_value=str(uuid.uuid4())) + valid_nhs_number1, + VaccineTypes.covid_19, + imms_identifier_value=str(uuid.uuid4()), + ) imms_id = self.store_records(stored_records) # Retrieve the resources to get the identifier system and value via read API @@ -159,43 +164,38 @@ class SearchTestParams(NamedTuple): expected_status_code: int = 200 searches = [ - SearchTestParams( - "GET", - "", - None, - False, - 400 - ), - # No results. - SearchTestParams( - "GET", - f"identifier={identifier_system}|{identifier_value}", - None, - True, - 200 - ), - SearchTestParams( - "POST", - "", - f"identifier={identifier_system}|{identifier_value}", - True, - 200 - ), - SearchTestParams( - "POST", - f"identifier={identifier_system}|{identifier_value}", - f"identifier={identifier_system}|{identifier_value}", - False, - 400 - ), - ] + SearchTestParams("GET", "", None, False, 400), + # No results. + SearchTestParams( + "GET", + f"identifier={identifier_system}|{identifier_value}", + None, + True, + 200, + ), + SearchTestParams( + "POST", + "", + f"identifier={identifier_system}|{identifier_value}", + True, + 200, + ), + SearchTestParams( + "POST", + f"identifier={identifier_system}|{identifier_value}", + f"identifier={identifier_system}|{identifier_value}", + False, + 400, + ), + ] for search in searches: pprint.pprint(search) response = self.default_imms_api.search_immunizations_full( search.method, search.query_string, body=search.body, - expected_status_code=search.expected_status_code) + expected_status_code=search.expected_status_code, + ) # Then assert response.ok == search.should_be_success, response.text diff --git a/e2e/test_search_identifier_elements_immunization.py b/e2e/test_search_identifier_elements_immunization.py index c346429d6..da6c3d130 100644 --- a/e2e/test_search_identifier_elements_immunization.py +++ b/e2e/test_search_identifier_elements_immunization.py @@ -1,11 +1,12 @@ -from utils.base_test import ImmunizationBaseTest -from lib.env import get_service_base_path import pprint import uuid +from typing import NamedTuple, Literal, Optional + +from lib.env import get_service_base_path +from utils.base_test import ImmunizationBaseTest from utils.constants import valid_nhs_number1 -from utils.resource import generate_imms_resource from utils.mappings import VaccineTypes -from typing import NamedTuple, Literal, Optional +from utils.resource import generate_imms_resource class TestSearchImmunizationByIdentifier(ImmunizationBaseTest): @@ -33,7 +34,8 @@ def test_search_imms(self): # When search_response = imms_api.search_immunization_by_identifier_and_elements( - identifier_system, identifier_value) + identifier_system, identifier_value + ) self.assertEqual(search_response.status_code, 200, search_response.text) bundle = search_response.json() self.assertEqual(bundle.get("resourceType"), "Bundle", bundle) @@ -45,7 +47,8 @@ def test_search_imms(self): self.assertEqual(entries[0]["resource"]["meta"]["versionId"], 1) self.assertTrue(entries[0]["fullUrl"].startswith("https://")) self.assertEqual( - entries[0]["fullUrl"], f"{get_service_base_path()}/Immunization/{covid_ids}" + entries[0]["fullUrl"], + f"{get_service_base_path()}/Immunization/{covid_ids}", ) def test_search_imms_no_match_returns_empty_bundle(self): @@ -63,8 +66,10 @@ def test_search_imms_no_match_returns_empty_bundle(self): def test_search_by_identifier_parameter_smoke_tests(self): stored_records = generate_imms_resource( - valid_nhs_number1, VaccineTypes.covid_19, - imms_identifier_value=str(uuid.uuid4())) + valid_nhs_number1, + VaccineTypes.covid_19, + imms_identifier_value=str(uuid.uuid4()), + ) imms_id = self.store_records(stored_records) # Retrieve the resources to get the identifier system and value via read API @@ -85,43 +90,38 @@ class SearchTestParams(NamedTuple): expected_status_code: int = 200 searches = [ - SearchTestParams( - "GET", - "", - None, - False, - 400 - ), - # No results. - SearchTestParams( - "GET", - f"identifier={identifier_system}|{identifier_value}", - None, - True, - 200 - ), - SearchTestParams( - "POST", - "", - f"identifier={identifier_system}|{identifier_value}", - True, - 200 - ), - SearchTestParams( - "POST", - f"identifier={identifier_system}|{identifier_value}", - f"identifier={identifier_system}|{identifier_value}", - False, - 400 - ), - ] + SearchTestParams("GET", "", None, False, 400), + # No results. + SearchTestParams( + "GET", + f"identifier={identifier_system}|{identifier_value}", + None, + True, + 200, + ), + SearchTestParams( + "POST", + "", + f"identifier={identifier_system}|{identifier_value}", + True, + 200, + ), + SearchTestParams( + "POST", + f"identifier={identifier_system}|{identifier_value}", + f"identifier={identifier_system}|{identifier_value}", + False, + 400, + ), + ] for search in searches: pprint.pprint(search) response = self.default_imms_api.search_immunizations_full( search.method, search.query_string, body=search.body, - expected_status_code=search.expected_status_code) + expected_status_code=search.expected_status_code, + ) # Then assert response.ok == search.should_be_success, response.text diff --git a/e2e/test_search_immunization.py b/e2e/test_search_immunization.py index 7906e3fb7..12ae7bf76 100644 --- a/e2e/test_search_immunization.py +++ b/e2e/test_search_immunization.py @@ -1,12 +1,18 @@ import pprint import uuid -from typing import NamedTuple, Literal, Optional, List from decimal import Decimal +from typing import NamedTuple, Literal, Optional, List + +from lib.env import get_service_base_path from utils.base_test import ImmunizationBaseTest -from utils.constants import valid_nhs_number1, valid_nhs_number2, valid_patient_identifier2, valid_patient_identifier1 -from utils.resource import generate_imms_resource, generate_filtered_imms_resource +from utils.constants import ( + valid_nhs_number1, + valid_nhs_number2, + valid_patient_identifier2, + valid_patient_identifier1, +) from utils.mappings import VaccineTypes -from lib.env import get_service_base_path +from utils.resource import generate_imms_resource, generate_filtered_imms_resource class TestSearchImmunization(ImmunizationBaseTest): @@ -119,7 +125,10 @@ def test_search_backwards_compatible(self): self.assertTrue(response_patient["fullUrl"].startswith("urn:uuid:")) self.assertTrue(uuid.UUID(response_patient["fullUrl"].split(":")[2])) expected_patient_resource_keys = ["resourceType", "id", "identifier"] - self.assertEqual(sorted(response_patient["resource"].keys()), sorted(expected_patient_resource_keys)) + self.assertEqual( + sorted(response_patient["resource"].keys()), + sorted(expected_patient_resource_keys), + ) self.assertEqual(response_patient["resource"]["id"], valid_nhs_number1) patient_identifier = response_patient["resource"]["identifier"] # NOTE: If PDS response ever changes to send more than one identifier then the below will break @@ -132,7 +141,8 @@ def test_search_backwards_compatible(self): expected_imms_resource["patient"]["reference"] = response_patient["fullUrl"] response_imm = next(item for item in entries if item["resource"]["id"] == imms_id) self.assertEqual( - response_imm["fullUrl"], f"{get_service_base_path()}/Immunization/{imms_id}" + response_imm["fullUrl"], + f"{get_service_base_path()}/Immunization/{imms_id}", ) self.assertEqual(response_imm["search"], {"mode": "match"}) self.assertEqual(response_imm["resource"], expected_imms_resource) @@ -162,17 +172,43 @@ def test_search_immunization_parameter_smoke_tests(self): time_1 = "2024-01-30T13:28:17.271+00:00" time_2 = "2024-02-01T13:28:17.271+00:00" stored_records = [ - generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr, imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number1, VaccineTypes.flu, imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number1, VaccineTypes.covid_19, imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number1, VaccineTypes.covid_19, - occurrence_date_time=time_1, - imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number1, VaccineTypes.covid_19, - occurrence_date_time=time_2, - imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number2, VaccineTypes.flu, imms_identifier_value=str(uuid.uuid4())), - generate_imms_resource(valid_nhs_number2, VaccineTypes.covid_19, imms_identifier_value=str(uuid.uuid4())), + generate_imms_resource( + valid_nhs_number1, + VaccineTypes.mmr, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number1, + VaccineTypes.flu, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number1, + VaccineTypes.covid_19, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number1, + VaccineTypes.covid_19, + occurrence_date_time=time_1, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number1, + VaccineTypes.covid_19, + occurrence_date_time=time_2, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number2, + VaccineTypes.flu, + imms_identifier_value=str(uuid.uuid4()), + ), + generate_imms_resource( + valid_nhs_number2, + VaccineTypes.covid_19, + imms_identifier_value=str(uuid.uuid4()), + ), ] created_resource_ids = list(self.store_records(*stored_records)) @@ -188,14 +224,7 @@ class SearchTestParams(NamedTuple): expected_status_code: int = 200 searches = [ - SearchTestParams( - "GET", - "", - None, - False, - [], - 400 - ), + SearchTestParams("GET", "", None, False, [], 400), # No results. SearchTestParams( "GET", @@ -203,7 +232,7 @@ class SearchTestParams(NamedTuple): None, True, [], - 200 + 200, ), # Basic success. SearchTestParams( @@ -212,7 +241,7 @@ class SearchTestParams(NamedTuple): None, True, [0], - 200 + 200, ), # "Or" params. SearchTestParams( @@ -222,7 +251,7 @@ class SearchTestParams(NamedTuple): None, True, [0, 1], - 200 + 200, ), # GET does not support body. SearchTestParams( @@ -231,7 +260,7 @@ class SearchTestParams(NamedTuple): f"patient.identifier={valid_patient_identifier1}", True, [0], - 200 + 200, ), SearchTestParams( "POST", @@ -239,7 +268,7 @@ class SearchTestParams(NamedTuple): f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", True, [0], - 200 + 200, ), # Duplicated NHS number not allowed, spread across query and content. SearchTestParams( @@ -248,7 +277,7 @@ class SearchTestParams(NamedTuple): f"patient.identifier={valid_patient_identifier1}", False, [], - 400 + 400, ), SearchTestParams( "GET", @@ -258,7 +287,7 @@ class SearchTestParams(NamedTuple): None, False, [], - 400 + 400, ), # "And" params not supported. SearchTestParams( @@ -268,7 +297,7 @@ class SearchTestParams(NamedTuple): None, False, [], - 400 + 400, ), # Date SearchTestParams( @@ -277,7 +306,7 @@ class SearchTestParams(NamedTuple): None, True, [2, 3, 4], - 200 + 200, ), SearchTestParams( "GET", @@ -286,7 +315,7 @@ class SearchTestParams(NamedTuple): None, True, [3, 4], - 200 + 200, ), SearchTestParams( "GET", @@ -295,7 +324,7 @@ class SearchTestParams(NamedTuple): None, True, [2, 3], - 200 + 200, ), SearchTestParams( "GET", @@ -304,7 +333,7 @@ class SearchTestParams(NamedTuple): None, True, [3], - 200 + 200, ), # "from" after "to" is an error. SearchTestParams( @@ -314,15 +343,18 @@ class SearchTestParams(NamedTuple): None, False, [0], - 400 + 400, ), ] for search in searches: pprint.pprint(search) - response = self.default_imms_api.search_immunizations_full(search.method, search.query_string, - body=search.body, - expected_status_code=search.expected_status_code) + response = self.default_imms_api.search_immunizations_full( + search.method, + search.query_string, + body=search.body, + expected_status_code=search.expected_status_code, + ) # Then assert response.ok == search.should_be_success, response.text @@ -369,7 +401,7 @@ def test_search_immunization_accepts_include_and_provides_patient(self): response_without_include = self.default_imms_api.search_immunizations_full( "POST", f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - body=None + body=None, ) assert response_without_include.ok @@ -402,8 +434,7 @@ def test_search_reject_tbc(self): self.store_records(imms) # When - response = self.default_imms_api.search_immunizations("TBC", f"{VaccineTypes.mmr}", - expected_status_code=400) + response = self.default_imms_api.search_immunizations("TBC", f"{VaccineTypes.mmr}", expected_status_code=400) # Then self.assert_operation_outcome(response, 400) diff --git a/e2e/test_sqs_dlq.py b/e2e/test_sqs_dlq.py index eead7c018..6a76ee67e 100644 --- a/e2e/test_sqs_dlq.py +++ b/e2e/test_sqs_dlq.py @@ -1,10 +1,12 @@ import json -import boto3 -import unittest import os +import unittest + +import boto3 +from botocore.exceptions import ClientError # Handle potential errors + from utils.delete_sqs_messages import read_and_delete_messages from utils.get_sqs_url import get_queue_url -from botocore.exceptions import ClientError # Handle potential errors class TestSQS(unittest.TestCase): @@ -22,9 +24,7 @@ def test_send_message(self): sqs_client = boto3.client("sqs") try: # Send the message to the queue - response = sqs_client.send_message( - QueueUrl=self.queue_url, MessageBody=json.dumps(message_body) - ) + response = sqs_client.send_message(QueueUrl=self.queue_url, MessageBody=json.dumps(message_body)) read_and_delete_messages(self.queue_url) # Assert successful message sending self.assertIn("MessageId", response) diff --git a/e2e/test_update_immunization.py b/e2e/test_update_immunization.py index 709522d7f..98c934e90 100644 --- a/e2e/test_update_immunization.py +++ b/e2e/test_update_immunization.py @@ -1,5 +1,6 @@ import copy import uuid + from utils.base_test import ImmunizationBaseTest from utils.immunisation_api import parse_location from utils.resource import generate_imms_resource @@ -14,7 +15,7 @@ def test_update_imms(self): # Given immunization_resources = [ generate_imms_resource(), - generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event") + generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event"), ] for imms in immunization_resources: diff --git a/e2e/utils/base_test.py b/e2e/utils/base_test.py index e0d9c517b..405db1654 100644 --- a/e2e/utils/base_test.py +++ b/e2e/utils/base_test.py @@ -1,6 +1,10 @@ import unittest import uuid from typing import List + +from utils.constants import cis2_user +from utils.immunisation_api import ImmunisationApi + from lib.apigee import ApigeeService, ApigeeApp, ApigeeProduct from lib.authentication import ( AppRestrictedAuthentication, @@ -12,14 +16,12 @@ get_proxy_name, get_service_base_path, ) -from utils.constants import cis2_user from utils.factories import ( make_apigee_service, make_app_restricted_app, make_cis2_app, make_apigee_product, ) -from utils.immunisation_api import ImmunisationApi class ImmunizationBaseTest(unittest.TestCase): diff --git a/e2e/utils/batch.py b/e2e/utils/batch.py index 0d18471a9..eeb8576d9 100644 --- a/e2e/utils/batch.py +++ b/e2e/utils/batch.py @@ -156,9 +156,7 @@ def _get_terraform_output(output_name: str) -> str: text=True, ) if output.returncode != 0: - raise RuntimeError( - f"Error getting terraform output {output_name}: {output.stderr}" - ) + raise RuntimeError(f"Error getting terraform output {output_name}: {output.stderr}") return output.stdout.strip() @@ -254,7 +252,5 @@ def add_record(self, record: OrderedDict[str, str], msg: str = ""): def upload_to_s3(self, s3_client, bucket): self.stream.seek(0) - s3_client.upload_fileobj( - self.stream, bucket, ExtraArgs={"ContentType": "text/plain"} - ) + s3_client.upload_fileobj(self.stream, bucket, ExtraArgs={"ContentType": "text/plain"}) self.stream.close() diff --git a/e2e/utils/delete_sqs_messages.py b/e2e/utils/delete_sqs_messages.py index 3d286a5ba..d5ed82e2a 100644 --- a/e2e/utils/delete_sqs_messages.py +++ b/e2e/utils/delete_sqs_messages.py @@ -11,9 +11,7 @@ def read_and_delete_messages(queue_url): sqs_client = boto3.client("sqs") try: # Receive messages with a maximum of 10 messages per request - response = sqs_client.receive_message( - QueueUrl=queue_url, MaxNumberOfMessages=10 - ) + response = sqs_client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10) # Check if there are any messages if "Messages" not in response: diff --git a/e2e/utils/factories.py b/e2e/utils/factories.py index ecd1a2c93..a7b155ba7 100644 --- a/e2e/utils/factories.py +++ b/e2e/utils/factories.py @@ -50,9 +50,7 @@ def make_app_restricted_auth( return AppRestrictedAuthentication(auth_url=get_auth_url(), config=config) -def make_apigee_product( - apigee: ApigeeService = None, product: ApigeeProduct = None -) -> ApigeeProduct: +def make_apigee_product(apigee: ApigeeService = None, product: ApigeeProduct = None) -> ApigeeProduct: if not apigee: apigee = make_apigee_service() if not product: @@ -95,9 +93,7 @@ def make_app_restricted_app( key_id = f"{key_id_prefix}-{str(uuid.uuid4())}" jwks_data = JwksData(key_id) - jwks_url = jwks_data.get_jwks_url( - base_url="https://api.service.nhs.uk/mock-jwks" - ) + jwks_url = jwks_data.get_jwks_url(base_url="https://api.service.nhs.uk/mock-jwks") app.add_attribute("jwks-resource-url", jwks_url) if permissions := permissions or app_full_access(): @@ -113,7 +109,7 @@ def make_app_restricted_app( "VaccineTypePermissions", "flu:create,covid19:create,mmr:create,hpv:create,covid19:update,flu:read,covid19:read,flu:delete," "covid19:delete,mmr:delete,flu:search,covid19:search,mmr:search,rsv:create,rsv:search,rsv:update," - "rsv:read,rsv:delete" + "rsv:read,rsv:delete", ) app.add_product(f"identity-service-{get_apigee_env()}") @@ -157,7 +153,7 @@ def _make_user_restricted_app( "VaccineTypePermissions", "flu:create,covid19:create,mmr:create,hpv:create,covid19:update,flu:read,covid19:read,flu:delete," "covid19:delete,mmr:delete,flu:search,covid19:search,mmr:search,rsv:create,rsv:search,rsv:update," - "rsv:read,rsv:delete" + "rsv:read,rsv:delete", ) app.add_product(f"identity-service-{get_apigee_env()}") @@ -171,9 +167,7 @@ def make_cis2_app( permissions: Set[Permission] = None, vaxx_type_perms: Set = None, ) -> (ApigeeApp, UserRestrictedCredentials): - stored_app = _make_user_restricted_app( - AuthType.CIS2, apigee, app, permissions, vaxx_type_perms - ) + stored_app = _make_user_restricted_app(AuthType.CIS2, apigee, app, permissions, vaxx_type_perms) credentials = UserRestrictedCredentials( client_id=stored_app.get_client_id(), client_secret=stored_app.get_client_secret(), diff --git a/e2e/utils/immunisation_api.py b/e2e/utils/immunisation_api.py index 0bc517880..dda191e2a 100644 --- a/e2e/utils/immunisation_api.py +++ b/e2e/utils/immunisation_api.py @@ -1,13 +1,14 @@ +import random import re -import uuid import time -import random -import requests -from utils.resource import generate_imms_resource, delete_imms_records -from typing import Optional, Literal, List +import uuid from datetime import datetime +from typing import Optional, Literal, List + +import requests from lib.authentication import BaseAuthentication +from utils.resource import generate_imms_resource, delete_imms_records from .constants import patient_identifier_system @@ -36,7 +37,8 @@ def __init__(self, url, auth: BaseAuthentication): self.headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json", - "Accept": "application/fhir+json"} + "Accept": "application/fhir+json", + } self.generated_test_records = [] def __str__(self): @@ -56,7 +58,7 @@ def make_request_with_backoff( expected_connection_failure: bool = False, max_retries: int = 5, is_status_check: bool = False, - **kwargs + **kwargs, ): for attempt in range(max_retries): try: @@ -75,17 +77,19 @@ def make_request_with_backoff( if is_status_check: body = response.json() if body["status"].lower() != "pass": - raise RuntimeError(f"Server status check at {url} returned status code {response.status_code}, " - f"but status is: {body['status']}") + raise RuntimeError( + f"Server status check at {url} returned status code {response.status_code}, " + f"but status is: {body['status']}" + ) # Check if the response matches the expected status code to identify potential issues if response.status_code != expected_status_code: if response.status_code >= 500: - raise RuntimeError(f"Server error: {response.status_code} during " - f"in {http_method} {url}") + raise RuntimeError(f"Server error: {response.status_code} during " f"in {http_method} {url}") else: - raise ValueError(f"Expected {expected_status_code} but got " - f"{response.status_code} in {http_method} {url}") + raise ValueError( + f"Expected {expected_status_code} but got " f"{response.status_code} in {http_method} {url}" + ) return response @@ -94,7 +98,7 @@ def make_request_with_backoff( raise # This is will be used in the retry logic of the exponential backoff - delay = (3 ** attempt) + random.uniform(0, 0.5) + delay = (3**attempt) + random.uniform(0, 0.5) print( f"[{datetime.now():%Y-%m-%d %H:%M:%S}] " f"[Retry {attempt + 1}] {http_method.upper()} {url} — {e} — retrying in {delay:.2f}s" @@ -126,7 +130,7 @@ def get_immunization_by_id(self, event_id, expected_status_code: int = 200): http_method="GET", url=f"{self.url}/Immunization/{event_id}", headers=self._update_headers(), - expected_status_code=expected_status_code + expected_status_code=expected_status_code, ) # Create a new Immunization resource by sending a POST request to the API @@ -137,7 +141,7 @@ def create_immunization(self, imms, expected_status_code: int = 201): url=f"{self.url}/Immunization", headers=self._update_headers(), expected_status_code=expected_status_code, - json=imms + json=imms, ) if response.status_code == 201: @@ -158,7 +162,7 @@ def update_immunization(self, imms_id, imms, expected_status_code: int = 200, he url=f"{self.url}/Immunization/{imms_id}", headers=self._update_headers(headers), expected_status_code=expected_status_code, - json=imms + json=imms, ) def delete_immunization(self, imms_id, expected_status_code: int = 204): @@ -169,41 +173,53 @@ def delete_immunization(self, imms_id, expected_status_code: int = 204): expected_status_code=expected_status_code, ) - def search_immunizations(self, patient_identifier: str, immunization_target: str, expected_status_code: int = 200): + def search_immunizations( + self, + patient_identifier: str, + immunization_target: str, + expected_status_code: int = 200, + ): return self.make_request_with_backoff( http_method="GET", url=f"{self.url}/Immunization?patient.identifier={patient_identifier_system}|{patient_identifier}" f"&-immunization.target={immunization_target}", headers=self._update_headers(), - expected_status_code=expected_status_code + expected_status_code=expected_status_code, ) def search_immunization_by_identifier( - self, identifier_system: str, - identifier_value: str, expected_status_code: int = 200): + self, + identifier_system: str, + identifier_value: str, + expected_status_code: int = 200, + ): return self.make_request_with_backoff( http_method="GET", url=f"{self.url}/Immunization?identifier={identifier_system}|{identifier_value}", headers=self._update_headers(), - expected_status_code=expected_status_code + expected_status_code=expected_status_code, ) def search_immunization_by_identifier_and_elements( - self, identifier_system: str, - identifier_value: str, expected_status_code: int = 200): + self, + identifier_system: str, + identifier_value: str, + expected_status_code: int = 200, + ): return self.make_request_with_backoff( http_method="GET", url=f"{self.url}/Immunization?identifier={identifier_system}|{identifier_value}&_elements=id,meta", headers=self._update_headers(), - expected_status_code=expected_status_code + expected_status_code=expected_status_code, ) def search_immunizations_full( - self, - http_method: Literal["POST", "GET"], - query_string: Optional[str], - body: Optional[str], - expected_status_code: int = 200): + self, + http_method: Literal["POST", "GET"], + query_string: Optional[str], + body: Optional[str], + expected_status_code: int = 200, + ): if http_method == "POST": url = f"{self.url}/Immunization/_search?{query_string}" @@ -215,18 +231,21 @@ def search_immunizations_full( url=url, headers=self._update_headers({"Content-Type": "application/x-www-form-urlencoded"}), expected_status_code=expected_status_code, - data=body + data=body, ) def _update_headers(self, headers=None): if headers is None: headers = {} - updated = {**self.headers, **{ - "X-Correlation-ID": str(uuid.uuid4()), - "X-Request-ID": str(uuid.uuid4()), - "E-Tag": "1", - "Accept": "application/fhir+json" - }} + updated = { + **self.headers, + **{ + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "E-Tag": "1", + "Accept": "application/fhir+json", + }, + } return {**updated, **headers} def _is_valid_uuid4(self, imms_id): diff --git a/e2e/utils/resource.py b/e2e/utils/resource.py index 4c236c758..e974cf3e5 100644 --- a/e2e/utils/resource.py +++ b/e2e/utils/resource.py @@ -1,14 +1,16 @@ import json import os import uuid -import boto3 from copy import deepcopy from decimal import Decimal from typing import Union, Literal -from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table + +import boto3 from botocore.config import Config -from .mappings import vaccine_type_mappings, VaccineTypes +from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table + from .constants import valid_nhs_number1 +from .mappings import vaccine_type_mappings, VaccineTypes current_directory = os.path.dirname(os.path.realpath(__file__)) diff --git a/e2e_batch/README.md b/e2e_batch/README.md index 47047b543..48cf9a61c 100644 --- a/e2e_batch/README.md +++ b/e2e_batch/README.md @@ -3,36 +3,46 @@ This test suite provides automated end-to-end (E2E) testing for the Immunisation FHIR API batch processing pipeline. It verifies that batch file submissions are correctly processed, acknowledged, and validated across the system. ## Overview + - Framework: Python unittest - Purpose: Simulate real-world batch file submissions, poll for acknowledgements, and validate processing results. - Test Scenarios: Defined in the scenarios module and enabled in setUp(). - Key Features: -- - Uploads test batch files to S3. -- - Waits for and validates ACK (acknowledgement) files. -- - Cleans up SQS queues and test artifacts after each run. +- - Uploads test batch files to S3. +- - Waits for and validates ACK (acknowledgement) files. +- - Cleans up SQS queues and test artifacts after each run. ## Test Flow + 1. Setup (setUp) + - Loads and enables a set of test scenarios. - Prepares test data for batch submission. + 2. Test Execution (test_batch_submission) + - Uploads ALL enabled test files to S3. - Polls for ALL ACK responses and forwarded files. - Validates the content and structure of ACK files. + 3. Teardown (tearDown) + - Cleans up SQS queues and any generated test files. ## Key Functions + - send_files(tests): Uploads enabled test files to the S3 input bucket. - poll_for_responses(tests, max_timeout): Polls for ACKs and processed files, with a timeout. - validate_responses(tests): Validates the content of ACK files and checks for expected outcomes. ## How to Run + 1. Ensure all dependencies and environment variables are set (see project root README). 2. Update `.env` file with contents indicated in `PR-NNN.env`, modified for PR 3. Update `.env` with referrence to the appropriate AWS config profile `AWS_PROFILE={your-aws-profile}` 4. Update the apigee app to match the required PR-NNN -5. Run tests from vscode debugger or from makefile using +5. Run tests from vscode debugger or from makefile using + ``` make test -``` \ No newline at end of file +``` diff --git a/e2e_batch/clients.py b/e2e_batch/clients.py index 474b5293a..1cd05f050 100644 --- a/e2e_batch/clients.py +++ b/e2e_batch/clients.py @@ -4,9 +4,12 @@ import logging from constants import ( - environment, REGION, - batch_fifo_queue_name, ack_metadata_queue_name, audit_table_name - ) + environment, + REGION, + batch_fifo_queue_name, + ack_metadata_queue_name, + audit_table_name, +) from boto3 import client as boto3_client, resource as boto3_resource @@ -16,12 +19,12 @@ s3_client = boto3_client("s3", region_name=REGION) dynamodb = boto3_resource("dynamodb", region_name=REGION) -sqs_client = boto3_client('sqs', region_name=REGION) +sqs_client = boto3_client("sqs", region_name=REGION) events_table_name = f"imms-{environment}-imms-events" events_table = dynamodb.Table(events_table_name) audit_table = dynamodb.Table(audit_table_name) -batch_fifo_queue_url = sqs_client.get_queue_url(QueueName=batch_fifo_queue_name)['QueueUrl'] -ack_metadata_queue_url = sqs_client.get_queue_url(QueueName=ack_metadata_queue_name)['QueueUrl'] +batch_fifo_queue_url = sqs_client.get_queue_url(QueueName=batch_fifo_queue_name)["QueueUrl"] +ack_metadata_queue_url = sqs_client.get_queue_url(QueueName=ack_metadata_queue_name)["QueueUrl"] # Logger logging.basicConfig(level="INFO") logger = logging.getLogger() diff --git a/e2e_batch/constants.py b/e2e_batch/constants.py index c522618b3..eb866e8e8 100644 --- a/e2e_batch/constants.py +++ b/e2e_batch/constants.py @@ -92,6 +92,9 @@ class TestSet: CREATE_OK = ActionSequence("Create. OK", [ActionFlag.CREATE]) UPDATE_OK = ActionSequence("Update. OK", [ActionFlag.CREATE, ActionFlag.UPDATE]) DELETE_OK = ActionSequence("Delete. OK", [ActionFlag.CREATE, ActionFlag.UPDATE, ActionFlag.DELETE_LOGICAL]) - REINSTATE_OK = ActionSequence("Reinstate. OK", [ActionFlag.CREATE, ActionFlag.DELETE_LOGICAL, ActionFlag.UPDATE]) + REINSTATE_OK = ActionSequence( + "Reinstate. OK", + [ActionFlag.CREATE, ActionFlag.DELETE_LOGICAL, ActionFlag.UPDATE], + ) DELETE_FAIL = ActionSequence("Delete without Create. Fail", [ActionFlag.DELETE_LOGICAL]) UPDATE_FAIL = ActionSequence("Update without Create. Fail", [ActionFlag.UPDATE], outcome=ActionFlag.NONE) diff --git a/e2e_batch/scenarios.py b/e2e_batch/scenarios.py index cd70b036d..436c30d67 100644 --- a/e2e_batch/scenarios.py +++ b/e2e_batch/scenarios.py @@ -2,13 +2,17 @@ from datetime import datetime, timezone from vax_suppliers import TestPair, OdsVax from constants import ( - ActionFlag, BusRowResult, DestinationType, Operation, + ActionFlag, + BusRowResult, + DestinationType, + Operation, ACK_BUCKET, RAVS_URI, - OperationOutcome + OperationOutcome, ) from utils import ( - poll_s3_file_pattern, fetch_pk_and_operation_from_dynamodb, + poll_s3_file_pattern, + fetch_pk_and_operation_from_dynamodb, validate_fatal_error, get_file_content_from_s3, aws_cleanup, @@ -21,9 +25,12 @@ class TestAction: - def __init__(self, action: ActionFlag, - expected_header_response_code=BusRowResult.SUCCESS, - expected_operation_outcome=''): + def __init__( + self, + action: ActionFlag, + expected_header_response_code=BusRowResult.SUCCESS, + expected_operation_outcome="", + ): self.action = action self.expected_header_response_code = expected_header_response_code self.expected_operation_outcome = expected_operation_outcome @@ -45,9 +52,9 @@ def __init__(self, scenario: dict): self.enabled = scenario.get("enabled", False) self.ack_keys = {DestinationType.INF: None, DestinationType.BUS: None} # initialise attribs to be set later - self.key = None # S3 key of the uploaded file - self.file_name = None # name of the generated CSV file - self.identifier = None # unique identifier of subject in the CSV file rows + self.key = None # S3 key of the uploaded file + self.file_name = None # name of the generated CSV file + self.identifier = None # unique identifier of subject in the CSV file rows def get_poll_destinations(self, pending: bool) -> bool: # loop through keys in test (inf and bus) @@ -113,11 +120,17 @@ def check_bus_file_content(self): def generate_csv_file(self): self.file_name = self.get_file_name(self.vax, self.ods, self.version) - logger.info(f"Test \"{self.name}\" File {self.file_name}") + logger.info(f'Test "{self.name}" File {self.file_name}') data = [] self.identifier = str(uuid.uuid4()) for action in self.actions: - row = create_row(self.identifier, self.dose_amount, action.action, self.header, self.inject_cp1252) + row = create_row( + self.identifier, + self.dose_amount, + action.action, + self.header, + self.inject_cp1252, + ) logger.info(f" > {action.action} - {self.vax}/{self.ods} - {self.identifier}") data.append(row) df = pd.DataFrame(data) @@ -164,39 +177,50 @@ def generate_csv_files(test_cases: list[TestCase]) -> list[TestCase]: "ods_vax": TestPair.E8HA94_COVID19_CUD, "operation_outcome": ActionFlag.CREATE, "actions": [TestAction(ActionFlag.CREATE)], - "description": "Successful Create" + "description": "Successful Create", }, { "name": "Successful Update", "description": "Successful Create,Update", "ods_vax": TestPair.DPSFULL_COVID19_CRUDS, "operation_outcome": ActionFlag.UPDATE, - "actions": [TestAction(ActionFlag.CREATE), TestAction(ActionFlag.UPDATE)] + "actions": [TestAction(ActionFlag.CREATE), TestAction(ActionFlag.UPDATE)], }, { "name": "Successful Delete", "description": "Successful Create,Update, Delete", "ods_vax": TestPair.V0V8L_FLU_CRUDS, "operation_outcome": ActionFlag.DELETE_LOGICAL, - "actions": [TestAction(ActionFlag.CREATE), TestAction(ActionFlag.DELETE_LOGICAL)] + "actions": [ + TestAction(ActionFlag.CREATE), + TestAction(ActionFlag.DELETE_LOGICAL), + ], }, { "name": "Failed Update", "description": "Failed Update - resource does not exist", "ods_vax": TestPair.V0V8L_3IN1_CRUDS, - "actions": [TestAction(ActionFlag.UPDATE, - expected_header_response_code=BusRowResult.FATAL_ERROR, - expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND)], - "operation_outcome": ActionFlag.NONE + "actions": [ + TestAction( + ActionFlag.UPDATE, + expected_header_response_code=BusRowResult.FATAL_ERROR, + expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND, + ) + ], + "operation_outcome": ActionFlag.NONE, }, { "name": "Failed Delete", "description": "Failed Delete - resource does not exist", "ods_vax": TestPair.X26_MMR_CRUDS, - "actions": [TestAction(ActionFlag.DELETE_LOGICAL, - expected_header_response_code=BusRowResult.FATAL_ERROR, - expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND)], - "operation_outcome": ActionFlag.NONE + "actions": [ + TestAction( + ActionFlag.DELETE_LOGICAL, + expected_header_response_code=BusRowResult.FATAL_ERROR, + expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND, + ) + ], + "operation_outcome": ActionFlag.NONE, }, { "name": "Create with 1252 char", @@ -204,8 +228,8 @@ def generate_csv_files(test_cases: list[TestCase]) -> list[TestCase]: "ods_vax": TestPair.YGA_MENACWY_CRUDS, "operation_outcome": ActionFlag.CREATE, "actions": [TestAction(ActionFlag.CREATE)], - "create_with_cp1252_encoded_character": True - } - ], - "ref": [] - } + "create_with_cp1252_encoded_character": True, + }, + ], + "ref": [], +} diff --git a/e2e_batch/test_e2e_batch.py b/e2e_batch/test_e2e_batch.py index 6147a375a..b55b737ff 100644 --- a/e2e_batch/test_e2e_batch.py +++ b/e2e_batch/test_e2e_batch.py @@ -6,11 +6,17 @@ check_ack_file_content, validate_row_count, purge_sqs_queues, - delete_file_from_s3 + delete_file_from_s3, ) from clients import logger -from scenarios import scenarios, TestCase, create_test_cases, enable_tests, generate_csv_files +from scenarios import ( + scenarios, + TestCase, + create_test_cases, + enable_tests, + generate_csv_files, +) from constants import ( SOURCE_BUCKET, @@ -18,21 +24,24 @@ ACK_BUCKET, environment, DestinationType, - TEMP_ACK_PREFIX + TEMP_ACK_PREFIX, ) class TestE2EBatch(unittest.TestCase): def setUp(self): self.tests: list[TestCase] = create_test_cases(scenarios["dev"]) - enable_tests(self.tests, [ - "Successful Create", - "Successful Update", - "Successful Delete", - "Create with 1252 char", - "Failed Update", - "Failed Delete", - ]) + enable_tests( + self.tests, + [ + "Successful Create", + "Successful Update", + "Successful Delete", + "Create with 1252 char", + "Failed Update", + "Failed Delete", + ], + ) generate_csv_files(self.tests) def tearDown(self): @@ -98,16 +107,18 @@ def validate_responses(tests: list[TestCase]): if test.ack_keys[DestinationType.INF]: count += 1 inf_ack_content = get_file_content_from_s3(ACK_BUCKET, test.ack_keys[DestinationType.INF]) - check_ack_file_content(test.name, inf_ack_content, "Success", None, - test.operation_outcome) + check_ack_file_content(test.name, inf_ack_content, "Success", None, test.operation_outcome) else: logger.error(f"INF ACK file not found for test: {test.name}") errors = True if test.ack_keys[DestinationType.BUS]: count += 1 - validate_row_count(f"{test.name} - bus", test.file_name, - test.ack_keys[DestinationType.BUS]) + validate_row_count( + f"{test.name} - bus", + test.file_name, + test.ack_keys[DestinationType.BUS], + ) test.check_bus_file_content() diff --git a/e2e_batch/utils.py b/e2e_batch/utils.py index 560b6ed96..d3a4347af 100644 --- a/e2e_batch/utils.py +++ b/e2e_batch/utils.py @@ -11,8 +11,13 @@ from io import StringIO from datetime import datetime, timezone from clients import ( - logger, s3_client, audit_table, events_table, sqs_client, - batch_fifo_queue_url, ack_metadata_queue_url + logger, + s3_client, + audit_table, + events_table, + sqs_client, + batch_fifo_queue_url, + ack_metadata_queue_url, ) from errors import AckFileNotFoundError, DynamoDBMismatchError from constants import ( @@ -25,7 +30,7 @@ HEADER_RESPONSE_CODE_COLUMN, RAVS_URI, ActionFlag, - environment + environment, ) @@ -157,7 +162,8 @@ def check_ack_file_content(desc, content, response_code, operation_outcome, oper row_HEADER_RESPONSE_CODE = row["HEADER_RESPONSE_CODE"].strip() assert row_HEADER_RESPONSE_CODE == response_code, ( f"{desc}.Row {i} expected HEADER_RESPONSE_CODE '{response_code}', " - f"but got '{row_HEADER_RESPONSE_CODE}'") + f"but got '{row_HEADER_RESPONSE_CODE}'" + ) if operation_outcome and "OPERATION_OUTCOME" in row: assert row["OPERATION_OUTCOME"].strip() == operation_outcome, ( f"Row {i} expected OPERATION_OUTCOME '{operation_outcome}', " @@ -286,7 +292,11 @@ def fetch_pk_and_operation_from_dynamodb(identifier_pk): items = response["Items"] if items: if "DeletedAt" in items[0]: - return (items[0]["PK"], items[0]["Operation"], items[0]["DeletedAt"]) + return ( + items[0]["PK"], + items[0]["Operation"], + items[0]["DeletedAt"], + ) return (items[0]["PK"], items[0]["Operation"], None) return (identifier_pk, ActionFlag.NONE, None) @@ -337,13 +347,25 @@ def generate_csv_with_ordered_100000_rows(file_name=None): # Generate first 300 rows as structured NEW → UPDATE → DELETE sets for i in range(special_row_count // 3): # 100 sets new_row = create_row( - unique_id=unique_ids[i], fore_name="PHYLIS", dose_amount="0.3", action_flag="NEW", header="NHS_NUMBER" + unique_id=unique_ids[i], + fore_name="PHYLIS", + dose_amount="0.3", + action_flag="NEW", + header="NHS_NUMBER", ) update_row = create_row( - unique_id=unique_ids[i], fore_name="PHYLIS", dose_amount="0.4", action_flag="UPDATE", header="NHS_NUMBER" + unique_id=unique_ids[i], + fore_name="PHYLIS", + dose_amount="0.4", + action_flag="UPDATE", + header="NHS_NUMBER", ) delete_row = create_row( - unique_id=unique_ids[i], fore_name="PHYLIS", dose_amount="0.1", action_flag="DELETE", header="NHS_NUMBER" + unique_id=unique_ids[i], + fore_name="PHYLIS", + dose_amount="0.1", + action_flag="DELETE", + header="NHS_NUMBER", ) special_data.append((new_row, update_row, delete_row)) # Keep them as ordered tuples @@ -357,7 +379,11 @@ def generate_csv_with_ordered_100000_rows(file_name=None): # Generate remaining 99,700 rows as CREATE operations create_data = [ create_row( - unique_id=str(uuid.uuid4()), action_flag="NEW", dose_amount="0.3", fore_name="PHYLIS", header="NHS_NUMBER" + unique_id=str(uuid.uuid4()), + action_flag="NEW", + dose_amount="0.3", + fore_name="PHYLIS", + header="NHS_NUMBER", ) for _ in range(total_rows - special_row_count) ] @@ -369,7 +395,13 @@ def generate_csv_with_ordered_100000_rows(file_name=None): random.shuffle(full_data) # Sort data so that within each unique ID, "NEW" appears before "UPDATE" and "DELETE" - full_data.sort(key=lambda x: (x["UNIQUE_ID"], x["ACTION_FLAG"] != "NEW", x["ACTION_FLAG"] == "DELETE")) + full_data.sort( + key=lambda x: ( + x["UNIQUE_ID"], + x["ACTION_FLAG"] != "NEW", + x["ACTION_FLAG"] == "DELETE", + ) + ) # Convert to DataFrame and save as CSV df = pd.DataFrame(full_data) @@ -403,7 +435,7 @@ def delete_filename_from_audit_table(filename) -> bool: try: response = audit_table.query( IndexName="filename_index", - KeyConditionExpression=Key("filename").eq(filename) + KeyConditionExpression=Key("filename").eq(filename), ) items = response.get("Items", []) @@ -423,7 +455,7 @@ def delete_filename_from_events_table(identifier) -> bool: identifier_pk = f"{RAVS_URI}#{identifier}" response = events_table.query( IndexName="IdentifierGSI", - KeyConditionExpression=Key("IdentifierPK").eq(identifier_pk) + KeyConditionExpression=Key("IdentifierPK").eq(identifier_pk), ) items = response.get("Items", []) @@ -478,7 +510,7 @@ def purge_sqs_queues() -> bool: def create_row(unique_id, dose_amount, action_flag: str, header, inject_cp1252=None): """Helper function to create a single row with the specified UNIQUE_ID and ACTION_FLAG.""" - name = "James" if not inject_cp1252 else b'Jam\xe9s' + name = "James" if not inject_cp1252 else b"Jam\xe9s" return { header: "9732928395", "PERSON_FORENAME": "PHYLIS", diff --git a/e2e_batch/vax_suppliers.py b/e2e_batch/vax_suppliers.py index a4d85357c..aff4cc174 100644 --- a/e2e_batch/vax_suppliers.py +++ b/e2e_batch/vax_suppliers.py @@ -8,7 +8,7 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" + "RSV": "CRUDS", } }, "DPSREDUCED": { @@ -19,7 +19,7 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" + "RSV": "CRUDS", } }, "MAVIS": { @@ -28,28 +28,14 @@ "FLU": "CRUDS", "HPV": "CRUDS", "MENACWY": "CRUDS", - "MMR": "CRUDS" - } - }, - "SONAR": { - "8HK48": { - "FLU": "CD" - } - }, - "EVA": { - "8HA94": { - "COVID19": "CUD" + "MMR": "CRUDS", } }, + "SONAR": {"8HK48": {"FLU": "CD"}}, + "EVA": {"8HA94": {"COVID19": "CUD"}}, "RAVS": { - "X26": { - "MMR": "CRUDS", - "RSV": "CRUDS" - }, - "X8E5B": { - "MMR": "CRUDS", - "RSV": "CRUDS" - } + "X26": {"MMR": "CRUDS", "RSV": "CRUDS"}, + "X8E5B": {"MMR": "CRUDS", "RSV": "CRUDS"}, }, "EMIS": { "YGM41": { @@ -58,7 +44,7 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" + "RSV": "CRUDS", }, "YGJ": { "3IN1": "CRUDS", @@ -66,8 +52,8 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" - } + "RSV": "CRUDS", + }, }, "TPP": { "YGA": { @@ -75,7 +61,7 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" + "RSV": "CRUDS", } }, "MEDICUS": { @@ -84,9 +70,9 @@ "HPV": "CRUDS", "MENACWY": "CRUDS", "MMR": "CRUDS", - "RSV": "CRUDS" + "RSV": "CRUDS", } - } + }, } @@ -98,13 +84,14 @@ def __init__(self, ods_code: str, vax: str): class TestPair: """ - "ods_vax": TestPair.E8HA94_COVID19_CUD, - "ods_vax": TestPair.DPSFULL_COVID19_CRUDS, - "ods_vax": TestPair.V0V8L_FLU_CRUDS, - "ods_vax": TestPair.V0V8L_3IN1_CRUDS, - "ods_vax": TestPair.X26_MMR_CRUDS, - "ods_vax": TestPair.YGA_MENACWY_CRUDS, + "ods_vax": TestPair.E8HA94_COVID19_CUD, + "ods_vax": TestPair.DPSFULL_COVID19_CRUDS, + "ods_vax": TestPair.V0V8L_FLU_CRUDS, + "ods_vax": TestPair.V0V8L_3IN1_CRUDS, + "ods_vax": TestPair.X26_MMR_CRUDS, + "ods_vax": TestPair.YGA_MENACWY_CRUDS, """ + X26_MMR_CRUDS = OdsVax("X26", "MMR") # X26_RSV_CRUDS = OdsVax("X26", "RSV") # X8E5B_MMR_CRUDS = OdsVax("X8E5B", "MMR") diff --git a/filenameprocessor/src/audit_table.py b/filenameprocessor/src/audit_table.py index 31c05dc85..84f8efe41 100644 --- a/filenameprocessor/src/audit_table.py +++ b/filenameprocessor/src/audit_table.py @@ -1,4 +1,5 @@ """Add the filename to the audit table and check for duplicates.""" + from typing import Optional from clients import dynamodb_client, logger from errors import UnhandledAuditTableError @@ -12,7 +13,7 @@ def upsert_audit_table( expiry_timestamp: int, queue_name: str, file_status: str, - error_details: Optional[str] = None + error_details: Optional[str] = None, ) -> None: """ Updates the audit table with the file details @@ -23,7 +24,7 @@ def upsert_audit_table( AuditTableKeys.QUEUE_NAME: {"S": queue_name}, AuditTableKeys.STATUS: {"S": file_status}, AuditTableKeys.TIMESTAMP: {"S": created_at_formatted_str}, - AuditTableKeys.EXPIRES_AT: {"N": str(expiry_timestamp)} + AuditTableKeys.EXPIRES_AT: {"N": str(expiry_timestamp)}, } if error_details is not None: @@ -36,7 +37,11 @@ def upsert_audit_table( Item=audit_item, ConditionExpression="attribute_not_exists(message_id)", # Prevents accidental overwrites ) - logger.info("%s file, with message id %s, successfully added to audit table", file_key, message_id) + logger.info( + "%s file, with message id %s, successfully added to audit table", + file_key, + message_id, + ) except Exception as error: # pylint: disable = broad-exception-caught logger.error(error) diff --git a/filenameprocessor/src/constants.py b/filenameprocessor/src/constants.py index 43f6d264b..fe1fddef4 100644 --- a/filenameprocessor/src/constants.py +++ b/filenameprocessor/src/constants.py @@ -7,7 +7,7 @@ VaccineTypePermissionsError, InvalidFileKeyError, UnhandledAuditTableError, - UnhandledSqsError + UnhandledSqsError, ) SOURCE_BUCKET_NAME = os.getenv("SOURCE_BUCKET_NAME") @@ -40,6 +40,7 @@ class FileStatus(StrEnum): class FileNotProcessedReason(StrEnum): """Reasons why a file was not processed""" + EMPTY = "Empty file" UNAUTHORISED = "Unauthorised" diff --git a/filenameprocessor/src/elasticache.py b/filenameprocessor/src/elasticache.py index d430e19d2..7270d7a29 100644 --- a/filenameprocessor/src/elasticache.py +++ b/filenameprocessor/src/elasticache.py @@ -3,7 +3,7 @@ from constants import ( VACCINE_TYPE_TO_DISEASES_HASH_KEY, SUPPLIER_PERMISSIONS_HASH_KEY, - ODS_CODE_TO_SUPPLIER_SYSTEM_HASH_KEY + ODS_CODE_TO_SUPPLIER_SYSTEM_HASH_KEY, ) diff --git a/filenameprocessor/src/file_name_processor.py b/filenameprocessor/src/file_name_processor.py index 59e298464..6c3310639 100644 --- a/filenameprocessor/src/file_name_processor.py +++ b/filenameprocessor/src/file_name_processor.py @@ -20,9 +20,14 @@ VaccineTypePermissionsError, InvalidFileKeyError, UnhandledAuditTableError, - UnhandledSqsError + UnhandledSqsError, +) +from constants import ( + FileNotProcessedReason, + FileStatus, + ERROR_TYPE_TO_STATUS_CODE_MAP, + SOURCE_BUCKET_NAME, ) -from constants import FileNotProcessedReason, FileStatus, ERROR_TYPE_TO_STATUS_CODE_MAP, SOURCE_BUCKET_NAME # NOTE: logging_decorator is applied to handle_record function, rather than lambda_handler, because @@ -40,7 +45,11 @@ def handle_record(record) -> dict: except Exception as error: # pylint: disable=broad-except logger.error("Error obtaining file_key: %s", error) - return {"statusCode": 500, "message": "Failed to download file key", "error": str(error)} + return { + "statusCode": 500, + "message": "Failed to download file key", + "error": str(error), + } vaccine_type = "unknown" supplier = "unknown" @@ -71,10 +80,20 @@ def handle_record(record) -> dict: queue_name = f"{supplier}_{vaccine_type}" upsert_audit_table( - message_id, file_key, created_at_formatted_string, expiry_timestamp, queue_name, FileStatus.QUEUED + message_id, + file_key, + created_at_formatted_string, + expiry_timestamp, + queue_name, + FileStatus.QUEUED, ) make_and_send_sqs_message( - file_key, message_id, permissions, vaccine_type, supplier, created_at_formatted_string + file_key, + message_id, + permissions, + vaccine_type, + supplier, + created_at_formatted_string, ) logger.info("Lambda invocation successful for file '%s'", file_key) @@ -102,8 +121,13 @@ def handle_record(record) -> dict: file_status = get_file_status_for_error(error) upsert_audit_table( - message_id, file_key, created_at_formatted_string, expiry_timestamp, queue_name, file_status, - error_details=str(error) + message_id, + file_key, + created_at_formatted_string, + expiry_timestamp, + queue_name, + file_status, + error_details=str(error), ) # Create ack file @@ -121,7 +145,7 @@ def handle_record(record) -> dict: "message_id": message_id, "error": str(error), "vaccine_type": vaccine_type, - "supplier": supplier + "supplier": supplier, } @@ -138,19 +162,37 @@ def handle_unexpected_bucket_name(bucket_name: str, file_key: str) -> dict: config and overarching design""" try: vaccine_type, supplier = validate_file_key(file_key) - logger.error("Unable to process file %s due to unexpected bucket name %s", file_key, bucket_name) + logger.error( + "Unable to process file %s due to unexpected bucket name %s", + file_key, + bucket_name, + ) message = f"Failed to process file due to unexpected bucket name {bucket_name}" - return {"statusCode": 500, "message": message, "file_key": file_key, - "vaccine_type": vaccine_type, "supplier": supplier} + return { + "statusCode": 500, + "message": message, + "file_key": file_key, + "vaccine_type": vaccine_type, + "supplier": supplier, + } except Exception as error: - logger.error("Unable to process file due to unexpected bucket name %s and file key %s", - bucket_name, file_key) + logger.error( + "Unable to process file due to unexpected bucket name %s and file key %s", + bucket_name, + file_key, + ) message = f"Failed to process file due to unexpected bucket name {bucket_name} and file key {file_key}" - return {"statusCode": 500, "message": message, "file_key": file_key, - "vaccine_type": "unknown", "supplier": "unknown", "error": str(error)} + return { + "statusCode": 500, + "message": message, + "file_key": file_key, + "vaccine_type": "unknown", + "supplier": "unknown", + "error": str(error), + } def lambda_handler(event: dict, context) -> None: # pylint: disable=unused-argument @@ -170,16 +212,7 @@ def run_local(): parser.add_argument("--key", required=True, help="Object key.", type=str) args = parser.parse_args() - event = { - "Records": [ - { - "s3": { - "bucket": {"name": args.bucket}, - "object": {"key": args.key} - } - } - ] - } + event = {"Records": [{"s3": {"bucket": {"name": args.bucket}, "object": {"key": args.key}}}]} print(event) print(lambda_handler(event=event, context={})) diff --git a/filenameprocessor/src/file_validation.py b/filenameprocessor/src/file_validation.py index efe56464a..125393ec7 100644 --- a/filenameprocessor/src/file_validation.py +++ b/filenameprocessor/src/file_validation.py @@ -3,12 +3,15 @@ from re import match from datetime import datetime from constants import VALID_VERSIONS -from elasticache import get_valid_vaccine_types_from_cache, get_supplier_system_from_cache +from elasticache import ( + get_valid_vaccine_types_from_cache, + get_supplier_system_from_cache, +) from errors import InvalidFileKeyError def is_file_in_directory_root(file_key: str) -> bool: - """" + """ " Checks that a given file is in the bucket root rather than a child directory e.g. archive/xyz.csv """ return "/" not in file_key diff --git a/filenameprocessor/src/logging_decorator.py b/filenameprocessor/src/logging_decorator.py index c68c50988..c06f4f4c1 100644 --- a/filenameprocessor/src/logging_decorator.py +++ b/filenameprocessor/src/logging_decorator.py @@ -25,15 +25,20 @@ def generate_and_send_logs( base_log_data: dict, additional_log_data: dict, use_ms_precision: bool = False, - is_error_log: bool = False + is_error_log: bool = False, ) -> None: """Generates log data which includes the base_log_data, additional_log_data, and time taken (calculated using the current time and given start_time) and sends them to Cloudwatch and Firehose.""" seconds_elapsed = time.time() - start_time - formatted_time_elapsed = f"{round(seconds_elapsed * 1000, 5)}ms" if use_ms_precision else \ - f"{round(seconds_elapsed, 5)}s" + formatted_time_elapsed = ( + f"{round(seconds_elapsed * 1000, 5)}ms" if use_ms_precision else f"{round(seconds_elapsed, 5)}s" + ) - log_data = {**base_log_data, "time_taken": formatted_time_elapsed, **additional_log_data} + log_data = { + **base_log_data, + "time_taken": formatted_time_elapsed, + **additional_log_data, + } log_function = logger.error if is_error_log else logger.info log_function(json.dumps(log_data)) send_log_to_firehose(log_data) @@ -50,18 +55,31 @@ def logging_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - base_log_data = {"function_name": f"filename_processor_{func.__name__}", "date_time": str(datetime.now())} + base_log_data = { + "function_name": f"filename_processor_{func.__name__}", + "date_time": str(datetime.now()), + } start_time = time.time() try: result = func(*args, **kwargs) - generate_and_send_logs(start_time, base_log_data, additional_log_data=result, use_ms_precision=True) + generate_and_send_logs( + start_time, + base_log_data, + additional_log_data=result, + use_ms_precision=True, + ) return result except Exception as e: additional_log_data = {"statusCode": 500, "error": str(e)} - generate_and_send_logs(start_time, base_log_data, additional_log_data, is_error_log=True, - use_ms_precision=True) + generate_and_send_logs( + start_time, + base_log_data, + additional_log_data, + is_error_log=True, + use_ms_precision=True, + ) raise return wrapper diff --git a/filenameprocessor/src/make_and_upload_ack_file.py b/filenameprocessor/src/make_and_upload_ack_file.py index 889ce6da3..ccefba0ac 100644 --- a/filenameprocessor/src/make_and_upload_ack_file.py +++ b/filenameprocessor/src/make_and_upload_ack_file.py @@ -46,7 +46,10 @@ def upload_ack_file(file_key: str, ack_data: dict, created_at_formatted_string: def make_and_upload_the_ack_file( - message_id: str, file_key: str, message_delivered: bool, created_at_formatted_string: str + message_id: str, + file_key: str, + message_delivered: bool, + created_at_formatted_string: str, ) -> None: """Creates the ack file and uploads it to the S3 ack bucket""" ack_data = make_the_ack_data(message_id, message_delivered, created_at_formatted_string) diff --git a/filenameprocessor/src/send_sqs_message.py b/filenameprocessor/src/send_sqs_message.py index 0d8dad197..a5fb3c527 100644 --- a/filenameprocessor/src/send_sqs_message.py +++ b/filenameprocessor/src/send_sqs_message.py @@ -11,7 +11,9 @@ def send_to_supplier_queue(message_body: dict, vaccine_type: str, supplier: str) try: queue_url = os.getenv("QUEUE_URL") sqs_client.send_message( - QueueUrl=queue_url, MessageBody=json_dumps(message_body), MessageGroupId=f"{supplier}_{vaccine_type}" + QueueUrl=queue_url, + MessageBody=json_dumps(message_body), + MessageGroupId=f"{supplier}_{vaccine_type}", ) logger.info("Message sent to SQS queue for supplier: %s", supplier) except Exception as error: # pylint: disable=broad-exception-caught @@ -26,7 +28,7 @@ def make_and_send_sqs_message( permission: list[str], vaccine_type: str, supplier: str, - created_at_formatted_string: str + created_at_formatted_string: str, ) -> None: """Attempts to send a message to the SQS queue. Raises an exception if the message is not successfully sent.""" message_body = { diff --git a/filenameprocessor/src/utils_for_filenameprocessor.py b/filenameprocessor/src/utils_for_filenameprocessor.py index 88db408b2..d3ea2e1ff 100644 --- a/filenameprocessor/src/utils_for_filenameprocessor.py +++ b/filenameprocessor/src/utils_for_filenameprocessor.py @@ -1,4 +1,5 @@ """Utils for filenameprocessor lambda""" + from datetime import timedelta from clients import s3_client, logger from constants import AUDIT_TABLE_TTL_DAYS @@ -15,7 +16,9 @@ def get_creation_and_expiry_times(s3_response: dict) -> (str, int): def move_file(bucket_name: str, source_file_key: str, destination_file_key: str) -> None: """Moves a file from one location to another within a single S3 bucket by copying and then deleting the file.""" s3_client.copy_object( - Bucket=bucket_name, CopySource={"Bucket": bucket_name, "Key": source_file_key}, Key=destination_file_key + Bucket=bucket_name, + CopySource={"Bucket": bucket_name, "Key": source_file_key}, + Key=destination_file_key, ) s3_client.delete_object(Bucket=bucket_name, Key=source_file_key) logger.info("File moved from %s to %s", source_file_key, destination_file_key) diff --git a/filenameprocessor/tests/test_audit_table.py b/filenameprocessor/tests/test_audit_table.py index 6d18d65cb..0d958d2e0 100644 --- a/filenameprocessor/tests/test_audit_table.py +++ b/filenameprocessor/tests/test_audit_table.py @@ -6,7 +6,10 @@ from moto import mock_dynamodb from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT -from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown +from tests.utils_for_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) from tests.utils_for_tests.values_for_tests import MockFileDetails, FileDetails from tests.utils_for_tests.utils_for_filenameprocessor_tests import ( assert_audit_table_entry, @@ -55,7 +58,7 @@ def test_upsert_audit_table(self): created_at_formatted_str=ravs_rsv_test_file.created_at_formatted_string, queue_name=ravs_rsv_test_file.queue_name, file_status=FileStatus.PROCESSED, - expiry_timestamp=ravs_rsv_test_file.expires_at + expiry_timestamp=ravs_rsv_test_file.expires_at, ) assert_audit_table_entry(ravs_rsv_test_file, FileStatus.PROCESSED) @@ -70,7 +73,7 @@ def test_upsert_audit_table_with_duplicate_message_id_raises_exception(self): created_at_formatted_str=ravs_rsv_test_file.created_at_formatted_string, queue_name=ravs_rsv_test_file.queue_name, file_status=FileStatus.PROCESSED, - expiry_timestamp=ravs_rsv_test_file.expires_at + expiry_timestamp=ravs_rsv_test_file.expires_at, ) assert_audit_table_entry(ravs_rsv_test_file, FileStatus.PROCESSED) @@ -82,5 +85,5 @@ def test_upsert_audit_table_with_duplicate_message_id_raises_exception(self): created_at_formatted_str=ravs_rsv_test_file.created_at_formatted_string, queue_name=ravs_rsv_test_file.queue_name, file_status=FileStatus.PROCESSED, - expiry_timestamp=ravs_rsv_test_file.expires_at + expiry_timestamp=ravs_rsv_test_file.expires_at, ) diff --git a/filenameprocessor/tests/test_elasticache.py b/filenameprocessor/tests/test_elasticache.py index b224262a0..098ab9b7e 100644 --- a/filenameprocessor/tests/test_elasticache.py +++ b/filenameprocessor/tests/test_elasticache.py @@ -1,4 +1,5 @@ """Tests for elasticache functions""" + import json from unittest import TestCase from unittest.mock import patch @@ -6,7 +7,10 @@ from moto import mock_s3 from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT -from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown +from tests.utils_for_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) from tests.utils_for_tests.utils_for_filenameprocessor_tests import create_mock_hget # Ensure environment variables are mocked before importing from src files @@ -14,7 +18,7 @@ from elasticache import ( get_supplier_permissions_from_cache, get_valid_vaccine_types_from_cache, - get_supplier_system_from_cache + get_supplier_system_from_cache, ) from clients import REGION_NAME @@ -34,19 +38,19 @@ def tearDown(self): """Tear down the S3 buckets""" GenericTearDown(s3_client) - @patch("elasticache.redis_client.hget", side_effect=create_mock_hget( - {"TEST_ODS_CODE": "TEST_SUPPLIER"}, - {} - )) + @patch( + "elasticache.redis_client.hget", + side_effect=create_mock_hget({"TEST_ODS_CODE": "TEST_SUPPLIER"}, {}), + ) def test_get_supplier_system_from_cache(self, mock_hget): result = get_supplier_system_from_cache("TEST_ODS_CODE") self.assertEqual(result, "TEST_SUPPLIER") mock_hget.assert_called_once_with("ods_code_to_supplier", "TEST_ODS_CODE") - @patch("elasticache.redis_client.hget", side_effect=create_mock_hget( - {}, - {"TEST_SUPPLIER": json.dumps(["COVID19.CRUDS", "RSV.CRUDS"])} - )) + @patch( + "elasticache.redis_client.hget", + side_effect=create_mock_hget({}, {"TEST_SUPPLIER": json.dumps(["COVID19.CRUDS", "RSV.CRUDS"])}), + ) def test_get_supplier_permissions_from_cache(self, mock_hget): result = get_supplier_permissions_from_cache("TEST_SUPPLIER") self.assertEqual(result, ["COVID19.CRUDS", "RSV.CRUDS"]) diff --git a/filenameprocessor/tests/test_file_key_validation.py b/filenameprocessor/tests/test_file_key_validation.py index c3bee9987..a7ca2bc6d 100644 --- a/filenameprocessor/tests/test_file_key_validation.py +++ b/filenameprocessor/tests/test_file_key_validation.py @@ -5,11 +5,18 @@ from tests.utils_for_tests.values_for_tests import MockFileDetails from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT -from tests.utils_for_tests.utils_for_filenameprocessor_tests import MOCK_ODS_CODE_TO_SUPPLIER, create_mock_hget +from tests.utils_for_tests.utils_for_filenameprocessor_tests import ( + MOCK_ODS_CODE_TO_SUPPLIER, + create_mock_hget, +) # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): - from file_validation import is_file_in_directory_root, is_valid_datetime, validate_file_key + from file_validation import ( + is_file_in_directory_root, + is_valid_datetime, + validate_file_key, + ) from errors import InvalidFileKeyError VALID_FLU_EMIS_FILE_KEY = MockFileDetails.emis_flu.file_key @@ -18,6 +25,7 @@ class TestFileKeyValidation(TestCase): """Tests for file_key_validation functions""" + def test_is_file_in_directory_root(self): test_cases = [ ("test_file.csv", True), @@ -36,7 +44,10 @@ def test_is_valid_datetime(self): test_cases = [ ("20200101T12345600", True), # Valid datetime string with timezone ("20200101T123456", True), # Valid datetime string without timezone - ("20200101T123456extracharacters", True), # Valid datetime string with additional characters + ( + "20200101T123456extracharacters", + True, + ), # Valid datetime string with additional characters ("20201301T12345600", False), # Invalid month ("20200100T12345600", False), # Invalid day ("20200230T12345600", False), # Invalid combination of month and day @@ -51,7 +62,10 @@ def test_is_valid_datetime(self): with self.subTest(): self.assertEqual(is_valid_datetime(date_time_string), expected_result) - @patch("elasticache.redis_client.hget", side_effect=create_mock_hget(MOCK_ODS_CODE_TO_SUPPLIER, {})) + @patch( + "elasticache.redis_client.hget", + side_effect=create_mock_hget(MOCK_ODS_CODE_TO_SUPPLIER, {}), + ) @patch("elasticache.redis_client.hkeys", return_value=["FLU", "RSV"]) def test_validate_file_key(self, mock_hkeys, mock_hget): """Tests that file_key_validation returns True if all elements pass validation, and False otherwise""" @@ -66,7 +80,11 @@ def test_validate_file_key(self, mock_hkeys, mock_hget): # Valid RSV/ RAVS file key (VALID_RSV_RAVS_FILE_KEY, "X8E5B", ("RSV", "RAVS")), # VED-763 - Some suppliers may include ODS code at end of file for uniqueness - ("RSV_Vaccinations_v5_X8E5B_20000101T00000001_ODS123.csv", "X8E5B", ("RSV", "RAVS")), + ( + "RSV_Vaccinations_v5_X8E5B_20000101T00000001_ODS123.csv", + "X8E5B", + ("RSV", "RAVS"), + ), ] for file_key, ods_code, expected_result in test_cases_for_success_scenarios: @@ -80,41 +98,86 @@ def test_validate_file_key(self, mock_hkeys, mock_hget): missing_file_extension_error_message = "Initial file validation failed: missing file extension" test_cases_for_failure_scenarios = [ # File key with no '.' - (VALID_FLU_EMIS_FILE_KEY.replace(".", ""), missing_file_extension_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace(".", ""), + missing_file_extension_error_message, + ), # File key with additional '.' - (VALID_FLU_EMIS_FILE_KEY[:2] + "." + VALID_FLU_EMIS_FILE_KEY[2:], key_format_error_message), + ( + VALID_FLU_EMIS_FILE_KEY[:2] + "." + VALID_FLU_EMIS_FILE_KEY[2:], + key_format_error_message, + ), # File key with additional '_' - (VALID_FLU_EMIS_FILE_KEY[:2] + "_" + VALID_FLU_EMIS_FILE_KEY[2:], invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY[:2] + "_" + VALID_FLU_EMIS_FILE_KEY[2:], + invalid_file_key_error_message, + ), # File key with missing '_' (VALID_FLU_EMIS_FILE_KEY.replace("_", "", 1), key_format_error_message), # File key with missing '_' (VALID_FLU_EMIS_FILE_KEY.replace("_", ""), key_format_error_message), # File key with missing extension - (VALID_FLU_EMIS_FILE_KEY.replace(".csv", ""), missing_file_extension_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace(".csv", ""), + missing_file_extension_error_message, + ), # File key with invalid vaccine type - (VALID_FLU_EMIS_FILE_KEY.replace("FLU", "Flue"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("FLU", "Flue"), + invalid_file_key_error_message, + ), # File key with missing vaccine type - (VALID_FLU_EMIS_FILE_KEY.replace("FLU", ""), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("FLU", ""), + invalid_file_key_error_message, + ), # File key with invalid vaccinations element - (VALID_FLU_EMIS_FILE_KEY.replace("Vaccinations", "Vaccination"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("Vaccinations", "Vaccination"), + invalid_file_key_error_message, + ), # File key with missing vaccinations element - (VALID_FLU_EMIS_FILE_KEY.replace("Vaccinations", ""), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("Vaccinations", ""), + invalid_file_key_error_message, + ), # File key with invalid version - (VALID_FLU_EMIS_FILE_KEY.replace("v5", "v4"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("v5", "v4"), + invalid_file_key_error_message, + ), # File key with missing version (VALID_FLU_EMIS_FILE_KEY.replace("v5", ""), invalid_file_key_error_message), # File key with invalid ODS code - (VALID_FLU_EMIS_FILE_KEY.replace("YGM41", "YGAM"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("YGM41", "YGAM"), + invalid_file_key_error_message, + ), # File key with missing ODS code - (VALID_FLU_EMIS_FILE_KEY.replace("YGM41", ""), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("YGM41", ""), + invalid_file_key_error_message, + ), # File key with invalid timestamp - (VALID_FLU_EMIS_FILE_KEY.replace("20000101T00000001", "20200132T12345600"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("20000101T00000001", "20200132T12345600"), + invalid_file_key_error_message, + ), # File key with missing timestamp - (VALID_FLU_EMIS_FILE_KEY.replace("20000101T00000001", ""), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace("20000101T00000001", ""), + invalid_file_key_error_message, + ), # File key with incorrect extension - (VALID_FLU_EMIS_FILE_KEY.replace(".csv", ".xlsx"), invalid_file_key_error_message), + ( + VALID_FLU_EMIS_FILE_KEY.replace(".csv", ".xlsx"), + invalid_file_key_error_message, + ), # File key with ODS code but missing _ in the initial part of file key - ("MMR_Vaccinations_v5_DPSFULL20250910T11225000_test.csv", invalid_file_key_error_message) + ( + "MMR_Vaccinations_v5_DPSFULL20250910T11225000_test.csv", + invalid_file_key_error_message, + ), ] for file_key, expected_result in test_cases_for_failure_scenarios: diff --git a/filenameprocessor/tests/test_lambda_handler.py b/filenameprocessor/tests/test_lambda_handler.py index 99668a4e1..1c754dd11 100644 --- a/filenameprocessor/tests/test_lambda_handler.py +++ b/filenameprocessor/tests/test_lambda_handler.py @@ -1,4 +1,5 @@ """Tests for lambda_handler""" + import json import sys from unittest.mock import patch, ANY @@ -10,15 +11,26 @@ from boto3 import client as boto3_client from moto import mock_s3, mock_sqs, mock_firehose, mock_dynamodb -from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown +from tests.utils_for_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) from tests.utils_for_tests.utils_for_filenameprocessor_tests import ( assert_audit_table_entry, create_mock_hget, - MOCK_ODS_CODE_TO_SUPPLIER + MOCK_ODS_CODE_TO_SUPPLIER, +) +from tests.utils_for_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, + Sqs, +) +from tests.utils_for_tests.values_for_tests import ( + MOCK_CREATED_AT_FORMATTED_STRING, + MockFileDetails, + MOCK_BATCH_FILE_CONTENT, + MOCK_EXPIRES_AT, ) -from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, Sqs -from tests.utils_for_tests.values_for_tests import MOCK_CREATED_AT_FORMATTED_STRING, MockFileDetails, \ - MOCK_BATCH_FILE_CONTENT, MOCK_EXPIRES_AT # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): @@ -35,9 +47,7 @@ # for all vaccine types. This default is overridden for some specific tests. all_vaccine_types_in_this_test_file = ["RSV", "FLU"] all_suppliers_in_this_test_file = ["RAVS", "EMIS"] -all_permissions_in_this_test_file = [ - f"{vaccine_type}.CRUDS" for vaccine_type in all_vaccine_types_in_this_test_file -] +all_permissions_in_this_test_file = [f"{vaccine_type}.CRUDS" for vaccine_type in all_vaccine_types_in_this_test_file] @patch.dict("os.environ", MOCK_ENVIRONMENT_DICT) @@ -55,8 +65,7 @@ def run(self, result=None): after the test has run. """ mock_permissions_map = { - supplier: json.dumps(all_permissions_in_this_test_file) - for supplier in all_suppliers_in_this_test_file + supplier: json.dumps(all_permissions_in_this_test_file) for supplier in all_suppliers_in_this_test_file } mock_hget = create_mock_hget(MOCK_ODS_CODE_TO_SUPPLIER, mock_permissions_map) @@ -65,13 +74,18 @@ def run(self, result=None): # Patch get_creation_and_expiry_times, so that the ack file key can be deduced (it is already unittested # separately). Note that files numbered '1', which are predominantly used in these tests, use the # MOCK_CREATED_AT_FORMATTED_STRING. - patch("file_name_processor.get_creation_and_expiry_times", - return_value=(MOCK_CREATED_AT_FORMATTED_STRING, MOCK_EXPIRES_AT)), + patch( + "file_name_processor.get_creation_and_expiry_times", + return_value=(MOCK_CREATED_AT_FORMATTED_STRING, MOCK_EXPIRES_AT), + ), # Patch redis_client to use a fake redis client. patch("elasticache.redis_client", new=fakeredis.FakeStrictRedis()), # Patch the permissions config to allow all suppliers full permissions for all vaccine types. patch("elasticache.redis_client.hget", side_effect=mock_hget), - patch("elasticache.redis_client.hkeys", return_value=all_vaccine_types_in_this_test_file), + patch( + "elasticache.redis_client.hkeys", + return_value=all_vaccine_types_in_this_test_file, + ), ] with ExitStack() as stack: @@ -102,14 +116,20 @@ def make_record_with_message_id(file_key: str, message_id: str): Makes a record which includes a message_id, with the s3 bucket name set to BucketNames.SOURCE and s3 object key set to the file_key. """ - return {"s3": {"bucket": {"name": BucketNames.SOURCE}, "object": {"key": file_key}}, "message_id": message_id} + return { + "s3": {"bucket": {"name": BucketNames.SOURCE}, "object": {"key": file_key}}, + "message_id": message_id, + } def make_event(self, records: list): """Makes an event with s3 bucket name set to BucketNames.SOURCE and and s3 object key set to the file_key.""" return {"Records": records} @staticmethod - def get_ack_file_key(file_key: str, created_at_formatted_string: str = MOCK_CREATED_AT_FORMATTED_STRING) -> str: + def get_ack_file_key( + file_key: str, + created_at_formatted_string: str = MOCK_CREATED_AT_FORMATTED_STRING, + ) -> str: """Returns the ack file key for the given file key""" return f"ack/{file_key.replace('.csv', '_InfAck_' + created_at_formatted_string + '.csv')}" @@ -145,7 +165,8 @@ def assert_no_sqs_message(self) -> None: def assert_not_in_audit_table(self, file_details: MockFileDetails) -> None: """Assert that the file is not in the audit table""" table_entry = dynamodb_client.get_item( - TableName=AUDIT_TABLE_NAME, Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}} + TableName=AUDIT_TABLE_NAME, + Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}}, ).get("Item") self.assertIsNone(table_entry) @@ -184,10 +205,17 @@ def test_lambda_handler_new_file_success_and_first_in_queue(self): for file_details in test_cases: with self.subTest(file_details.name): # Set up the file in the source bucket - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key, Body=MOCK_BATCH_FILE_CONTENT) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=file_details.file_key, + Body=MOCK_BATCH_FILE_CONTENT, + ) with ( # noqa: E999 - patch("file_name_processor.uuid4", return_value=file_details.message_id), # noqa: E999 + patch( + "file_name_processor.uuid4", + return_value=file_details.message_id, + ), # noqa: E999 ): # noqa: E999 lambda_handler(self.make_event([self.make_record(file_details.file_key)]), None) @@ -208,7 +236,10 @@ def test_lambda_handler_non_root_file(self): with ( # noqa: E999 patch("file_name_processor.uuid4", return_value=file_details.message_id), # noqa: E999 ): # noqa: E999 - lambda_handler(self.make_event([self.make_record("folder/" + file_details.file_key)]), None) + lambda_handler( + self.make_event([self.make_record("folder/" + file_details.file_key)]), + None, + ) self.assert_not_in_audit_table(file_details) self.assert_no_sqs_message() @@ -222,7 +253,11 @@ def test_lambda_invalid_file_key_no_other_files_in_queue(self): * The failure inf_ack file is created """ invalid_file_key = "InvalidVaccineType_Vaccinations_v5_YGM41_20240708T12130100.csv" - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=invalid_file_key, Body=MOCK_BATCH_FILE_CONTENT) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=invalid_file_key, + Body=MOCK_BATCH_FILE_CONTENT, + ) file_details = deepcopy(MockFileDetails.ravs_rsv_1) file_details.file_key = invalid_file_key file_details.ack_file_key = self.get_ack_file_key(invalid_file_key) @@ -260,7 +295,11 @@ def test_lambda_invalid_permissions(self): * The failure inf_ack file is created """ file_details = MockFileDetails.ravs_rsv_1 - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key, Body=MOCK_BATCH_FILE_CONTENT) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=file_details.file_key, + Body=MOCK_BATCH_FILE_CONTENT, + ) # Mock the supplier permissions with a value which doesn't include the requested Flu permissions mock_hget = create_mock_hget({"X8E5B": "RAVS"}, {}) @@ -278,14 +317,16 @@ def test_lambda_invalid_permissions(self): "status": {"S": "Not processed - Unauthorised"}, "error_details": {"S": "Initial file validation failed: RAVS does not have permissions for RSV"}, "timestamp": {"S": file_details.created_at_formatted_string}, - "expires_at": {"N": str(file_details.expires_at)} + "expires_at": {"N": str(file_details.expires_at)}, } ] self.assertEqual(self.get_audit_table_items(), expected_table_items) self.assert_no_sqs_message() self.assert_ack_file_contents(file_details) - def test_lambda_adds_event_to_audit_table_as_failed_when_unexpected_exception_is_caught(self): + def test_lambda_adds_event_to_audit_table_as_failed_when_unexpected_exception_is_caught( + self, + ): """ Tests that when an unexpected error occurs e.g. an unexpected exception when validating permissions: * The file is added to the audit table with a status of 'Failed' and the reason @@ -293,13 +334,18 @@ def test_lambda_adds_event_to_audit_table_as_failed_when_unexpected_exception_is * The failure inf_ack file is created """ test_file_details = MockFileDetails.emis_flu - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=test_file_details.file_key, Body=MOCK_BATCH_FILE_CONTENT) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=test_file_details.file_key, + Body=MOCK_BATCH_FILE_CONTENT, + ) with ( # noqa: E999 patch("file_name_processor.uuid4", return_value=test_file_details.message_id), # noqa: E999 - patch("file_name_processor.validate_vaccine_type_permissions", side_effect=Exception( - "Some unexpected exception" - )) + patch( + "file_name_processor.validate_vaccine_type_permissions", + side_effect=Exception("Some unexpected exception"), + ), ): # noqa: E999 lambda_handler(self.make_event([self.make_record(test_file_details.file_key)]), None) @@ -330,7 +376,10 @@ class TestUnexpectedBucket(TestCase): def setUp(self): GenericSetUp(s3_client, firehose_client, sqs_client, dynamodb_client) - hkeys_patcher = patch("elasticache.redis_client.hkeys", return_value=all_vaccine_types_in_this_test_file) + hkeys_patcher = patch( + "elasticache.redis_client.hkeys", + return_value=all_vaccine_types_in_this_test_file, + ) self.addCleanup(hkeys_patcher.stop) hkeys_patcher.start() @@ -348,7 +397,7 @@ def test_unexpected_bucket_name(self): record = { "s3": { "bucket": {"name": "unknown-bucket"}, - "object": {"key": ravs_record.file_key} + "object": {"key": ravs_record.file_key}, } } @@ -373,7 +422,7 @@ def test_unexpected_bucket_name_and_filename_validation_fails(self): record = { "s3": { "bucket": {"name": "unknown-bucket"}, - "object": {"key": invalid_file_key} + "object": {"key": invalid_file_key}, } } @@ -398,8 +447,10 @@ class TestMainEntryPoint(TestCase): def test_run_local_constructs_event_and_calls_lambda_handler(self): test_args = [ "file_name_processor.py", - "--bucket", "test-bucket", - "--key", "some/path/file.csv" + "--bucket", + "test-bucket", + "--key", + "some/path/file.csv", ] expected_event = { @@ -407,7 +458,7 @@ def test_run_local_constructs_event_and_calls_lambda_handler(self): { "s3": { "bucket": {"name": "test-bucket"}, - "object": {"key": "some/path/file.csv"} + "object": {"key": "some/path/file.csv"}, } } ] @@ -416,9 +467,10 @@ def test_run_local_constructs_event_and_calls_lambda_handler(self): with ( patch.object(sys, "argv", test_args), patch("file_name_processor.lambda_handler") as mock_lambda_handler, - patch("file_name_processor.print") as mock_print + patch("file_name_processor.print") as mock_print, ): import file_name_processor + file_name_processor.run_local() mock_lambda_handler.assert_called_once_with(event=expected_event, context={}) diff --git a/filenameprocessor/tests/test_logging_decorator.py b/filenameprocessor/tests/test_logging_decorator.py index 0cecac76d..0dc3285f6 100644 --- a/filenameprocessor/tests/test_logging_decorator.py +++ b/filenameprocessor/tests/test_logging_decorator.py @@ -8,9 +8,20 @@ from botocore.exceptions import ClientError from moto import mock_s3, mock_firehose, mock_sqs, mock_dynamodb -from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown -from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, Firehose -from tests.utils_for_tests.values_for_tests import MockFileDetails, fixed_datetime, MOCK_BATCH_FILE_CONTENT +from tests.utils_for_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) +from tests.utils_for_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, + Firehose, +) +from tests.utils_for_tests.values_for_tests import ( + MockFileDetails, + fixed_datetime, + MOCK_BATCH_FILE_CONTENT, +) from tests.utils_for_tests.utils_for_filenameprocessor_tests import create_mock_hget # Ensure environment variables are mocked before importing from src files @@ -26,7 +37,14 @@ FILE_DETAILS = MockFileDetails.emis_flu MOCK_VACCINATION_EVENT = { - "Records": [{"s3": {"bucket": {"name": BucketNames.SOURCE}, "object": {"key": FILE_DETAILS.file_key}}}] + "Records": [ + { + "s3": { + "bucket": {"name": BucketNames.SOURCE}, + "object": {"key": FILE_DETAILS.file_key}, + } + } + ] } @@ -41,7 +59,11 @@ class TestLoggingDecorator(unittest.TestCase): def setUp(self): """Set up the mock AWS environment and upload a valid FLU/EMIS file example""" GenericSetUp(s3_client, firehose_client, sqs_client, dynamodb_client) - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=FILE_DETAILS.file_key, Body=MOCK_BATCH_FILE_CONTENT) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=FILE_DETAILS.file_key, + Body=MOCK_BATCH_FILE_CONTENT, + ) def tearDown(self): """Clean the mock AWS environment""" @@ -68,7 +90,7 @@ def run(self, result=None): # Time is incremented by 1.0 for each call to time.time for ease of testing. # Range is set to a large number (100) due to many calls being made to time.time for some tests. patch("logging_decorator.time.time", side_effect=[0.0 + i for i in range(100)]), - patch("clients.redis_client.hkeys", return_value=["FLU"]) + patch("clients.redis_client.hkeys", return_value=["FLU"]), ] # Set up the ExitStack. Note that patches need to be explicitly started so that they will be applied even when @@ -111,12 +133,36 @@ def test_generate_and_send_logs(self): start_time = 1672531200 test_cases = [ - ("Using standard log and seconds precision", False, False, - {"base_key": "base_value", "time_taken": "0.12346s", "additional_key": "additional_value"}), - ("Using error log and seconds precision", True, False, - {"base_key": "base_value", "time_taken": "0.12346s", "additional_key": "additional_value"}), - ("Using standard log and milliseconds precision", False, True, - {"base_key": "base_value", "time_taken": "123.456ms", "additional_key": "additional_value"}) + ( + "Using standard log and seconds precision", + False, + False, + { + "base_key": "base_value", + "time_taken": "0.12346s", + "additional_key": "additional_value", + }, + ), + ( + "Using error log and seconds precision", + True, + False, + { + "base_key": "base_value", + "time_taken": "0.12346s", + "additional_key": "additional_value", + }, + ), + ( + "Using standard log and milliseconds precision", + False, + True, + { + "base_key": "base_value", + "time_taken": "123.456ms", + "additional_key": "additional_value", + }, + ), ] for test_desc, use_error_log, use_ms_precision, expected_log_data in test_cases: @@ -127,8 +173,13 @@ def test_generate_and_send_logs(self): patch("logging_decorator.time") as mock_time, # noqa: E999 ): # noqa: E999 mock_time.time.return_value = 1672531200.123456 # Mocks end time to be 0.123456s after start - generate_and_send_logs(start_time, base_log_data, additional_log_data, is_error_log=use_error_log, - use_ms_precision=use_ms_precision) + generate_and_send_logs( + start_time, + base_log_data, + additional_log_data, + is_error_log=use_error_log, + use_ms_precision=use_ms_precision, + ) if use_error_log: log_data = json.loads(mock_logger.error.call_args[0][0]) @@ -141,10 +192,7 @@ def test_generate_and_send_logs(self): def test_logging_successful_validation(self): """Tests that the correct logs are sent to cloudwatch and splunk when file validation is successful""" # Mock full permissions so that validation will pass - mock_hget = create_mock_hget( - {"YGM41": "EMIS"}, - {"EMIS": json.dumps(["FLU.CRUDS"])} - ) + mock_hget = create_mock_hget({"YGM41": "EMIS"}, {"EMIS": json.dumps(["FLU.CRUDS"])}) with ( # noqa: E999 patch("file_name_processor.uuid4", return_value=FILE_DETAILS.message_id), # noqa: E999 patch("elasticache.redis_client.hget", side_effect=mock_hget), # noqa: E999 @@ -173,10 +221,7 @@ def test_logging_successful_validation(self): def test_logging_failed_validation(self): """Tests that the correct logs are sent to cloudwatch and splunk when file validation fails""" # Set up permissions for COVID19 only (file is for FLU), so that validation will fail - mock_hget = create_mock_hget( - {"YGM41": "EMIS"}, - {"EMIS": json.dumps(["COVID19.CRUDS"])} - ) + mock_hget = create_mock_hget({"YGM41": "EMIS"}, {"EMIS": json.dumps(["COVID19.CRUDS"])}) with ( # noqa: E999 patch("file_name_processor.uuid4", return_value=FILE_DETAILS.message_id), # noqa: E999 patch("elasticache.redis_client.hget", side_effect=mock_hget), # noqa: E999 @@ -195,7 +240,7 @@ def test_logging_failed_validation(self): "message_id": FILE_DETAILS.message_id, "error": "Initial file validation failed: EMIS does not have permissions for FLU", "vaccine_type": "FLU", - "supplier": "EMIS" + "supplier": "EMIS", } log_data = json.loads(mock_logger.info.call_args[0][0]) @@ -207,17 +252,17 @@ def test_logging_throws_exception(self): """Tests that exception is caught when failing to send message to Firehose""" firehose_exception = ClientError( error_response={"Error": {"Code": "ServiceUnavailable", "Message": "Service down"}}, - operation_name="PutRecord" + operation_name="PutRecord", ) - mock_hget = create_mock_hget( - {"YGM41": "EMIS"}, - {"EMIS": json.dumps(["FLU.CRUDS"])} - ) + mock_hget = create_mock_hget({"YGM41": "EMIS"}, {"EMIS": json.dumps(["FLU.CRUDS"])}) with ( patch("file_name_processor.uuid4", return_value=FILE_DETAILS.message_id), patch("elasticache.redis_client.hget", side_effect=mock_hget), - patch("logging_decorator.firehose_client.put_record", side_effect=firehose_exception), + patch( + "logging_decorator.firehose_client.put_record", + side_effect=firehose_exception, + ), patch("logging_decorator.logger") as mock_logger, ): lambda_handler(MOCK_VACCINATION_EVENT, context=None) diff --git a/filenameprocessor/tests/test_make_and_upload_ack_file.py b/filenameprocessor/tests/test_make_and_upload_ack_file.py index 46b9dc243..0005ca3db 100644 --- a/filenameprocessor/tests/test_make_and_upload_ack_file.py +++ b/filenameprocessor/tests/test_make_and_upload_ack_file.py @@ -6,13 +6,22 @@ from boto3 import client as boto3_client from moto import mock_s3 -from tests.utils_for_tests.utils_for_filenameprocessor_tests import get_csv_file_dict_reader -from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames +from tests.utils_for_tests.utils_for_filenameprocessor_tests import ( + get_csv_file_dict_reader, +) +from tests.utils_for_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, +) from tests.utils_for_tests.values_for_tests import MockFileDetails # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): - from make_and_upload_ack_file import make_the_ack_data, upload_ack_file, make_and_upload_the_ack_file + from make_and_upload_ack_file import ( + make_the_ack_data, + upload_ack_file, + make_and_upload_the_ack_file, + ) from clients import REGION_NAME @@ -46,7 +55,8 @@ class TestMakeAndUploadAckFile(TestCase): def setUp(self): """Set up the bucket for the ack files""" s3_client.create_bucket( - Bucket=BucketNames.DESTINATION, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=BucketNames.DESTINATION, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) def test_make_ack_data(self): diff --git a/filenameprocessor/tests/test_send_sqs_message.py b/filenameprocessor/tests/test_send_sqs_message.py index f282db9a8..054996e5c 100644 --- a/filenameprocessor/tests/test_send_sqs_message.py +++ b/filenameprocessor/tests/test_send_sqs_message.py @@ -95,7 +95,10 @@ def test_make_and_send_sqs_message_success(self): # Assert that correct message has reached the queue messages = sqs_client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1) - self.assertEqual(json_loads(messages["Messages"][0]["Body"]), deepcopy(FLU_EMIS_FILE_DETAILS.sqs_message_body)) + self.assertEqual( + json_loads(messages["Messages"][0]["Body"]), + deepcopy(FLU_EMIS_FILE_DETAILS.sqs_message_body), + ) def test_make_and_send_sqs_message_failure(self): """Test make_and_send_sqs_message function for a failure due to queue not existing""" diff --git a/filenameprocessor/tests/test_supplier_permissions.py b/filenameprocessor/tests/test_supplier_permissions.py index bee88478d..d373a9223 100644 --- a/filenameprocessor/tests/test_supplier_permissions.py +++ b/filenameprocessor/tests/test_supplier_permissions.py @@ -13,6 +13,7 @@ class TestSupplierPermissions(TestCase): """Tests for validate_vaccine_type_permissions function and its helper functions""" + def test_validate_vaccine_type_permissions(self): """ Tests that validate_vaccine_type_permissions returns True if supplier has permissions @@ -34,9 +35,13 @@ def test_validate_vaccine_type_permissions(self): for vaccine_type, vaccine_permissions in success_test_cases: with self.subTest(): - with patch("supplier_permissions.get_supplier_permissions_from_cache", return_value=vaccine_permissions): + with patch( + "supplier_permissions.get_supplier_permissions_from_cache", + return_value=vaccine_permissions, + ): self.assertEqual( - validate_vaccine_type_permissions(vaccine_type, "TEST_SUPPLIER"), vaccine_permissions + validate_vaccine_type_permissions(vaccine_type, "TEST_SUPPLIER"), + vaccine_permissions, ) # Test case tuples are stuctured as (vaccine_type, vaccine_permissions) @@ -48,7 +53,10 @@ def test_validate_vaccine_type_permissions(self): for vaccine_type, vaccine_permissions in failure_test_cases: with self.subTest(): - with patch("supplier_permissions.get_supplier_permissions_from_cache", return_value=vaccine_permissions): + with patch( + "supplier_permissions.get_supplier_permissions_from_cache", + return_value=vaccine_permissions, + ): with self.assertRaises(VaccineTypePermissionsError) as context: validate_vaccine_type_permissions(vaccine_type, "TEST_SUPPLIER") self.assertEqual( diff --git a/filenameprocessor/tests/test_utils_for_filenameprocessor.py b/filenameprocessor/tests/test_utils_for_filenameprocessor.py index ebf9e595e..692f9eea9 100644 --- a/filenameprocessor/tests/test_utils_for_filenameprocessor.py +++ b/filenameprocessor/tests/test_utils_for_filenameprocessor.py @@ -6,17 +6,20 @@ from moto import mock_s3 from boto3 import client as boto3_client -from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames -from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown +from tests.utils_for_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, +) +from tests.utils_for_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): from constants import AUDIT_TABLE_TTL_DAYS from clients import REGION_NAME - from utils_for_filenameprocessor import ( - get_creation_and_expiry_times, - move_file - ) + from utils_for_filenameprocessor import get_creation_and_expiry_times, move_file s3_client = boto3_client("s3", region_name=REGION_NAME) diff --git a/filenameprocessor/tests/utils_for_tests/generic_setup_and_teardown.py b/filenameprocessor/tests/utils_for_tests/generic_setup_and_teardown.py index 494cb4918..c7d79359d 100644 --- a/filenameprocessor/tests/utils_for_tests/generic_setup_and_teardown.py +++ b/filenameprocessor/tests/utils_for_tests/generic_setup_and_teardown.py @@ -2,7 +2,12 @@ from unittest.mock import patch -from tests.utils_for_tests.mock_environment_variables import BucketNames, MOCK_ENVIRONMENT_DICT, Sqs, Firehose +from tests.utils_for_tests.mock_environment_variables import ( + BucketNames, + MOCK_ENVIRONMENT_DICT, + Sqs, + Firehose, +) # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): @@ -20,7 +25,13 @@ class GenericSetUp: * If dynamodb_client is provided, creates the audit table """ - def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamodb_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + sqs_client=None, + dynamodb_client=None, + ): if s3_client: for bucket_name in [ @@ -30,7 +41,8 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo BucketNames.MOCK_FIREHOSE, ]: s3_client.create_bucket( - Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) if firehose_client: @@ -51,9 +63,7 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo dynamodb_client.create_table( TableName=AUDIT_TABLE_NAME, KeySchema=[{"AttributeName": AuditTableKeys.MESSAGE_ID, "KeyType": "HASH"}], - AttributeDefinitions=[ - {"AttributeName": AuditTableKeys.MESSAGE_ID, "AttributeType": "S"} - ], + AttributeDefinitions=[{"AttributeName": AuditTableKeys.MESSAGE_ID, "AttributeType": "S"}], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, ) @@ -61,7 +71,13 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo class GenericTearDown: """Performs generic tear down of mock resources""" - def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamodb_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + sqs_client=None, + dynamodb_client=None, + ): if s3_client: for bucket_name in [ diff --git a/filenameprocessor/tests/utils_for_tests/utils_for_filenameprocessor_tests.py b/filenameprocessor/tests/utils_for_tests/utils_for_filenameprocessor_tests.py index 96cb3c1aa..c7956735e 100644 --- a/filenameprocessor/tests/utils_for_tests/utils_for_filenameprocessor_tests.py +++ b/filenameprocessor/tests/utils_for_tests/utils_for_filenameprocessor_tests.py @@ -1,4 +1,5 @@ """Utils functions for filenameprocessor tests""" + from unittest.mock import patch from io import StringIO from boto3 import client as boto3_client @@ -15,13 +16,10 @@ AUDIT_TABLE_NAME, FileStatus, SUPPLIER_PERMISSIONS_HASH_KEY, - ODS_CODE_TO_SUPPLIER_SYSTEM_HASH_KEY + ODS_CODE_TO_SUPPLIER_SYSTEM_HASH_KEY, ) -MOCK_ODS_CODE_TO_SUPPLIER = { - "YGM41": "EMIS", - "X8E5B": "RAVS" -} +MOCK_ODS_CODE_TO_SUPPLIER = {"YGM41": "EMIS", "X8E5B": "RAVS"} dynamodb_client = boto3_client("dynamodb", region_name=REGION_NAME) @@ -42,9 +40,13 @@ def add_entry_to_table(file_details: MockFileDetails, file_status: FileStatus) - def assert_audit_table_entry(file_details: FileDetails, expected_status: str) -> None: """Assert that the file details are in the audit table""" table_entry = dynamodb_client.get_item( - TableName=AUDIT_TABLE_NAME, Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}} + TableName=AUDIT_TABLE_NAME, + Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}}, ).get("Item") - assert table_entry == {**file_details.audit_table_entry, "status": {"S": expected_status}} + assert table_entry == { + **file_details.audit_table_entry, + "status": {"S": expected_status}, + } def create_mock_hget( @@ -57,4 +59,5 @@ def mock_hget(key, field): if key == SUPPLIER_PERMISSIONS_HASH_KEY: return mock_supplier_permissions.get(field) return None + return mock_hget diff --git a/grafana/non-prod/docker/dashboards/APIGateway_rev11.json b/grafana/non-prod/docker/dashboards/APIGateway_rev11.json index 39fa4a2ac..d61acb89b 100644 --- a/grafana/non-prod/docker/dashboards/APIGateway_rev11.json +++ b/grafana/non-prod/docker/dashboards/APIGateway_rev11.json @@ -144,9 +144,7 @@ "period": "$agg", "refId": "A", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -305,9 +303,7 @@ "period": "$agg", "refId": "A", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -340,9 +336,7 @@ "period": "$agg", "refId": "B", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -375,9 +369,7 @@ "period": "$agg", "refId": "C", "region": "$region", - "statistics": [ - "Maximum" - ] + "statistics": ["Maximum"] } ], "thresholds": [], @@ -513,9 +505,7 @@ "period": "", "refId": "A", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -548,9 +538,7 @@ "period": "", "refId": "B", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -584,9 +572,7 @@ "period": "", "refId": "E", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "Total Error Rate", @@ -619,9 +605,7 @@ "period": "", "refId": "D", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -775,9 +759,7 @@ "period": "$agg", "refId": "A", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -811,9 +793,7 @@ "period": "$agg", "refId": "B", "region": "$region", - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -892,10 +872,7 @@ "refresh": false, "schemaVersion": 26, "style": "dark", - "tags": [ - "monitoringartist", - "cloudwatch" - ], + "tags": ["monitoringartist", "cloudwatch"], "templating": { "list": [ { @@ -1081,20 +1058,10 @@ "2h", "1d" ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] }, "timezone": "browser", "title": "AWS API Gateway", "uid": "AWSAPIGat", "version": 1 -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/dashboards/Dynamo_rev6.json b/grafana/non-prod/docker/dashboards/Dynamo_rev6.json index bb99f39fb..2828c4648 100644 --- a/grafana/non-prod/docker/dashboards/Dynamo_rev6.json +++ b/grafana/non-prod/docker/dashboards/Dynamo_rev6.json @@ -149,11 +149,7 @@ "id": 10, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -261,11 +257,7 @@ "id": 17, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -409,11 +401,7 @@ "id": 18, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -620,11 +608,7 @@ "id": 2, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -826,11 +810,7 @@ "id": 25, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "right", "showLegend": false, @@ -950,11 +930,7 @@ "id": 15, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": false, @@ -1201,11 +1177,7 @@ "id": 27, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": false, @@ -1324,11 +1296,7 @@ "id": 19, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": false, @@ -1448,11 +1416,7 @@ "id": 29, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": false, @@ -1671,11 +1635,7 @@ "id": 30, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": false, @@ -1902,11 +1862,7 @@ "id": 14, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2017,11 +1973,7 @@ "id": 26, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2260,11 +2212,7 @@ "id": 16, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2375,11 +2323,7 @@ "id": 28, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2491,11 +2435,7 @@ "id": 1, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2706,11 +2646,7 @@ "id": 7, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -2935,11 +2871,7 @@ "id": 8, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3102,11 +3034,7 @@ "id": 9, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3354,11 +3282,7 @@ "id": 12, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3496,11 +3420,7 @@ "id": 13, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3646,11 +3566,7 @@ "id": 5, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3854,11 +3770,7 @@ "id": 11, "options": { "legend": { - "calcs": [ - "min", - "max", - "lastNotNull" - ], + "calcs": ["min", "max", "lastNotNull"], "displayMode": "table", "placement": "bottom", "showLegend": true @@ -3901,9 +3813,7 @@ ], "refresh": "auto", "schemaVersion": 39, - "tags": [ - "cloudwatch" - ], + "tags": ["cloudwatch"], "templating": { "list": [ { @@ -4035,4 +3945,4 @@ "weekStart": "", "gnetId": 21974, "description": "AWS DynamoDB CloudWatch metrics" -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/dashboards/ECS_rev7.json b/grafana/non-prod/docker/dashboards/ECS_rev7.json index ba5b243dc..ea57f4785 100644 --- a/grafana/non-prod/docker/dashboards/ECS_rev7.json +++ b/grafana/non-prod/docker/dashboards/ECS_rev7.json @@ -49,9 +49,7 @@ "namespace": "AWS/ECS", "period": "", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ] }, @@ -131,9 +129,7 @@ "period": "", "refId": "A", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ], "thresholds": [], @@ -244,9 +240,7 @@ "period": "", "refId": "A", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ], "thresholds": [], @@ -360,9 +354,7 @@ "period": "", "refId": "A", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ], "thresholds": [], @@ -478,9 +470,7 @@ "period": "", "refId": "A", "region": "$region", - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ], "thresholds": [], @@ -557,10 +547,7 @@ ], "schemaVersion": 27, "style": "dark", - "tags": [ - "monitoringartist", - "cloudwatch" - ], + "tags": ["monitoringartist", "cloudwatch"], "templating": { "list": [ { @@ -670,20 +657,10 @@ "2h", "1d" ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] }, "timezone": "browser", "title": "AWS ECS", "uid": "eKASolEGk", "version": 7 -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/dashboards/Kinesis_rev5.json b/grafana/non-prod/docker/dashboards/Kinesis_rev5.json index 8524d5317..f913efeee 100644 --- a/grafana/non-prod/docker/dashboards/Kinesis_rev5.json +++ b/grafana/non-prod/docker/dashboards/Kinesis_rev5.json @@ -139,9 +139,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -175,9 +173,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -314,9 +310,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -350,9 +344,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -489,9 +481,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -628,9 +618,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -664,9 +652,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -798,9 +784,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -834,9 +818,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -973,9 +955,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -1009,9 +989,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Maximum" - ] + "statistics": ["Maximum"] } ], "thresholds": [], @@ -1088,10 +1066,7 @@ "refresh": false, "schemaVersion": 26, "style": "dark", - "tags": [ - "monitoringartist", - "cloudwatch" - ], + "tags": ["monitoringartist", "cloudwatch"], "templating": { "list": [ { @@ -1255,20 +1230,10 @@ "2h", "1d" ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] }, "timezone": "browser", "title": "AWS Kinesis", "uid": "AWSKinesi", "version": 1 -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/dashboards/Lambda_rev13.json b/grafana/non-prod/docker/dashboards/Lambda_rev13.json index 34c1d7fc7..a418adcf2 100644 --- a/grafana/non-prod/docker/dashboards/Lambda_rev13.json +++ b/grafana/non-prod/docker/dashboards/Lambda_rev13.json @@ -135,9 +135,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -171,9 +169,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Maximum" - ] + "statistics": ["Maximum"] } ], "thresholds": [], @@ -296,9 +292,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -424,9 +418,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -557,9 +549,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] }, { "alias": "{{metric}} {{stat}}", @@ -592,9 +582,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Sum" - ] + "statistics": ["Sum"] } ], "thresholds": [], @@ -723,9 +711,7 @@ "refId": "A", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] }, { "alias": "{{metric}} {{stat}}", @@ -756,9 +742,7 @@ "refId": "B", "region": "$region", "returnData": false, - "statistics": [ - "Average" - ] + "statistics": ["Average"] } ], "thresholds": [], @@ -824,10 +808,7 @@ "refresh": false, "schemaVersion": 19, "style": "dark", - "tags": [ - "monitoringartist", - "cloudwatch" - ], + "tags": ["monitoringartist", "cloudwatch"], "templating": { "list": [ { @@ -991,20 +972,10 @@ "2h", "1d" ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] }, "timezone": "browser", "title": "AWS Lambda", "uid": "AWSLambda", "version": 1 -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/dashboards/Redis_rev5.json b/grafana/non-prod/docker/dashboards/Redis_rev5.json index 92cb448e6..c8f22fabc 100644 --- a/grafana/non-prod/docker/dashboards/Redis_rev5.json +++ b/grafana/non-prod/docker/dashboards/Redis_rev5.json @@ -1745,10 +1745,7 @@ "refresh": false, "schemaVersion": 34, "style": "dark", - "tags": [ - "monitoringartist", - "cloudwatch" - ], + "tags": ["monitoringartist", "cloudwatch"], "templating": { "list": [ { @@ -1841,21 +1838,11 @@ "2h", "1d" ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] }, "timezone": "browser", "title": "AWS ElastiCache Redis", "uid": "AWSERedis", "version": 2, "weekStart": "" -} \ No newline at end of file +} diff --git a/grafana/non-prod/docker/provisioning/dashboards/dashboard.yml b/grafana/non-prod/docker/provisioning/dashboards/dashboard.yml index 2d2266dcc..57bc72eec 100644 --- a/grafana/non-prod/docker/provisioning/dashboards/dashboard.yml +++ b/grafana/non-prod/docker/provisioning/dashboards/dashboard.yml @@ -8,4 +8,4 @@ providers: disableDeletion: false updateIntervalSeconds: 10 options: - path: /var/lib/grafana/dashboards \ No newline at end of file + path: /var/lib/grafana/dashboards diff --git a/grafana/non-prod/docker/provisioning/datasources/datasource.yml b/grafana/non-prod/docker/provisioning/datasources/datasource.yml index e272f9bb7..f209a1979 100644 --- a/grafana/non-prod/docker/provisioning/datasources/datasource.yml +++ b/grafana/non-prod/docker/provisioning/datasources/datasource.yml @@ -7,74 +7,71 @@ deleteDatasources: orgId: 1 - name: Loki orgId: 1 - + datasources: - - name: DS_CLOUDWATCH # name of the datasource - type: cloudwatch # type of the data source - access: proxy # make grafana perform the requests - org_id: 1 # id of the organization to tie this datasource to - url: '' # url of the instance - password: # database password, if used - user: # database user, if used - database: # database name, if used - basicAuth: false # enable/disable basic auth - basicAuthUser: # basic auth username, if used - basicAuthPassword: # basic auth password, if used - withCredentials: false # enable/disable with credentials headers - isDefault: true # mark as default datasource. Max one per org - jsonData: # fields that will be converted to json and stored in json_data + - name: DS_CLOUDWATCH # name of the datasource + type: cloudwatch # type of the data source + access: proxy # make grafana perform the requests + org_id: 1 # id of the organization to tie this datasource to + url: "" # url of the instance + password: # database password, if used + user: # database user, if used + database: # database name, if used + basicAuth: false # enable/disable basic auth + basicAuthUser: # basic auth username, if used + basicAuthPassword: # basic auth password, if used + withCredentials: false # enable/disable with credentials headers + isDefault: true # mark as default datasource. Max one per org + jsonData: # fields that will be converted to json and stored in json_data authType: keys defaultRegion: eu-west-2 - editable: true # whether it should be editable + editable: true # whether it should be editable version: 1 - - - - name: 'Prometheus' # name of the datasource - type: 'prometheus' # type of the data source - access: 'proxy' # make grafana perform the requests - org_id: 1 # id of the organization to tie this datasource to - url: 'http://prometheus:9090' # url of the prom instance - password: # database password, if used - user: # database user, if used - database: # database name, if used - basicAuth: false # enable/disable basic auth - basicAuthUser: # basic auth username, if used - basicAuthPassword: # basic auth password, if used - withCredentials: # enable/disable with credentials headers - isDefault: false # mark as default datasource. Max one per org - jsonData: # fields that will be converted to json and stored in json_data + + - name: "Prometheus" # name of the datasource + type: "prometheus" # type of the data source + access: "proxy" # make grafana perform the requests + org_id: 1 # id of the organization to tie this datasource to + url: "http://prometheus:9090" # url of the prom instance + password: # database password, if used + user: # database user, if used + database: # database name, if used + basicAuth: false # enable/disable basic auth + basicAuthUser: # basic auth username, if used + basicAuthPassword: # basic auth password, if used + withCredentials: # enable/disable with credentials headers + isDefault: false # mark as default datasource. Max one per org + jsonData: # fields that will be converted to json and stored in json_data graphiteVersion: "1.1" tlsAuth: false tlsAuthWithCACert: false - secureJsonData: # json object of data that will be encrypted. + secureJsonData: # json object of data that will be encrypted. tlsCACert: "..." tlsClientCert: "..." tlsClientKey: "..." - editable: true # whether it should be editable + editable: true # whether it should be editable version: 1 - - name: 'Loki' # name of the datasource - type: 'loki' # type of the data source - access: 'proxy' # make grafana perform the requests - org_id: 1 # id of the organization to tie this datasource to - url: 'http://loki:3100' # url of the instance - password: # database password, if used - user: # database user, if used - database: # database name, if used - basicAuth: false # enable/disable basic auth - basicAuthUser: # basic auth username, if used - basicAuthPassword: # basic auth password, if used - withCredentials: # enable/disable with credentials headers - isDefault: false # mark as default datasource. Max one per org - jsonData: # fields that will be converted to json and stored in json_data + - name: "Loki" # name of the datasource + type: "loki" # type of the data source + access: "proxy" # make grafana perform the requests + org_id: 1 # id of the organization to tie this datasource to + url: "http://loki:3100" # url of the instance + password: # database password, if used + user: # database user, if used + database: # database name, if used + basicAuth: false # enable/disable basic auth + basicAuthUser: # basic auth username, if used + basicAuthPassword: # basic auth password, if used + withCredentials: # enable/disable with credentials headers + isDefault: false # mark as default datasource. Max one per org + jsonData: # fields that will be converted to json and stored in json_data graphiteVersion: "1.1" tlsAuth: false tlsAuthWithCACert: false - secureJsonData: # json object of data that will be encrypted. + secureJsonData: # json object of data that will be encrypted. tlsCACert: "..." tlsClientCert: "..." tlsClientKey: "..." - editable: true # whether it should be editable + editable: true # whether it should be editable version: 1 - - diff --git a/grafana/non-prod/docker/readme.md b/grafana/non-prod/docker/readme.md index 0a44becad..c14999b47 100644 --- a/grafana/non-prod/docker/readme.md +++ b/grafana/non-prod/docker/readme.md @@ -1,9 +1,11 @@ # Docker ## Introduction + This docker folder is used to deploy a grafana docker image to AWS ECR for use by ECS ## architecture + 1. Dockerfile uses grafana/grafana:latest and is built for linux/amd64 for deploy to ECS 2. Entrypoint script `run.sh` starts grafana in a controlled manner and permits debug on startup diff --git a/grafana/non-prod/readme.md b/grafana/non-prod/readme.md index 960151025..d189bc865 100644 --- a/grafana/non-prod/readme.md +++ b/grafana/non-prod/readme.md @@ -1,6 +1,7 @@ # Grafana infrastructure The build comes in 2 parts + 1. Docker image 2. AWS Infrastructure @@ -13,19 +14,23 @@ The code may be found in the docker folder. ## Infrastructure ### Terraform state -S3 bucket name : immunisation-grafana-terraform-state + +S3 bucket name : immunisation-grafana-terraform-state The infrastructure is built using terraform. The code may be found in the terraform folder. #### initialise terraform -The terraform amanges multiple environmments. When running terraform init is used to specify the key dynamically using the -backend-config flag. This is done in the tf_init.sh file. + +The terraform amanges multiple environmments. When running terraform init is used to specify the key dynamically using the -backend-config flag. This is done in the tf_init.sh file. to rebuild the docker image from the ECR to ECS, run + ``` terraform taint aws_ecs_task_definition.app ``` to review the docker image + ``` docker image inspect imms-internal-dev-fhir-api-grafana:11.0.0-22.04_stable docker image inspect imms-int-fhir-api-grafana:11.0.0-22.04_stable @@ -33,7 +38,9 @@ docker image inspect imms-ref-fhir-api-grafana:11.0.0-22.04_stable ``` ### building environments + Run the following commands to create and switch to the `int` workspace: + ``` ./tf_init.sh int ./tf_init.sh ref @@ -42,10 +49,12 @@ Run the following commands to create and switch to the `int` workspace: Create an environment ``` + terraform workspace new int Build an environment + ``` -terraform workspace select int +terraform workspace select int ``` ''' @@ -54,7 +63,8 @@ terraform plan -var="environment=int" ### vpce vs nat gateway -By default, grafana image requires access to internet for plugins and updates. +By default, grafana image requires access to internet for plugins and updates. + 1. Disable internet access. The updates can be disabled and plugins can be preloaded. However, this was timeboxed and timed out. 2. Permit access via VPC Endpoints. This gives access to AWS services. However updates & & info updates require internet access by default. To avoid a natgw, a proxy could be used. -3. NatGateway - this is the current solutipn. However, it should be reviewed as it is more permissive and has higher costs. \ No newline at end of file +3. NatGateway - this is the current solutipn. However, it should be reviewed as it is more permissive and has higher costs. diff --git a/grafana/non-prod/terraform/all.tf b/grafana/non-prod/terraform/all.tf index f9f502978..99d845999 100644 --- a/grafana/non-prod/terraform/all.tf +++ b/grafana/non-prod/terraform/all.tf @@ -19,7 +19,7 @@ resource "aws_alb_target_group" "app" { protocol = "HTTP" matcher = "200" timeout = 3 - path = "/api/health" # Grafana health check endpoint + path = "/api/health" # Grafana health check endpoint unhealthy_threshold = 2 } } @@ -99,36 +99,36 @@ resource "aws_appautoscaling_policy" "down" { # ecs.tf resource "aws_ecs_cluster" "main" { - name = "${local.prefix}-cluster" + name = "${local.prefix}-cluster" } data "template_file" "grafana_app" { - template = file("${path.module}/templates/ecs/grafana_app.json.tpl") - - vars = { - app_image = local.app_image - app_name = local.app_name - app_port = var.app_port - fargate_cpu = var.fargate_cpu - fargate_memory = var.fargate_memory - aws_region = var.aws_region - log_group = local.log_group - health_check_path = var.health_check_path - } + template = file("${path.module}/templates/ecs/grafana_app.json.tpl") + + vars = { + app_image = local.app_image + app_name = local.app_name + app_port = var.app_port + fargate_cpu = var.fargate_cpu + fargate_memory = var.fargate_memory + aws_region = var.aws_region + log_group = local.log_group + health_check_path = var.health_check_path + } } resource "aws_ecs_task_definition" "app" { - family = "${local.prefix}-app" - execution_role_arn = aws_iam_role.ecs_task_execution_role.arn - task_role_arn = aws_iam_role.ecs_task_role.arn - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = var.fargate_cpu - memory = var.fargate_memory - container_definitions = data.template_file.grafana_app.rendered - tags = merge(var.tags, { - Name = "${local.prefix}-task" - }) + family = "${local.prefix}-app" + execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.fargate_cpu + memory = var.fargate_memory + container_definitions = data.template_file.grafana_app.rendered + tags = merge(var.tags, { + Name = "${local.prefix}-task" + }) } @@ -209,16 +209,16 @@ resource "aws_iam_policy" "ecs_task_execution_policy" { Statement = [ { Effect = "Allow", - "Action": [ - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "ecr:BatchCheckLayerAvailability", - "ecr:GetAuthorizationToken", - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "s3:*" - ], + "Action" : [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:GetAuthorizationToken", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "s3:*" + ], Resource = "*" } ] @@ -260,7 +260,7 @@ resource "aws_iam_role" "ecs_task_role" { EOF } - # Resource = ${aws_iam_role.monitoring_role.arn} +# Resource = ${aws_iam_role.monitoring_role.arn} resource "aws_iam_policy" "ecs_task_policy" { @@ -273,9 +273,9 @@ resource "aws_iam_policy" "ecs_task_policy" { Effect = "Allow", Action = [ "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - ], + "logs:CreateLogStream", + "logs:PutLogEvents", + ], Resource = "*" } ] @@ -292,7 +292,7 @@ resource "aws_iam_role_policy_attachment" "task_s3" { data "aws_iam_policy_document" "ecs_auto_scale_role" { version = "2012-10-17" statement { - effect = "Allow" + effect = "Allow" actions = ["sts:AssumeRole"] principals { @@ -303,7 +303,7 @@ data "aws_iam_policy_document" "ecs_auto_scale_role" { } # ECS auto scale role resource "aws_iam_role" "ecs_auto_scale_role" { - name = "${local.prefix}-ecs_role" + name = "${local.prefix}-ecs_role" assume_role_policy = data.aws_iam_policy_document.ecs_auto_scale_role.json } # ECS auto scale role policy attachment @@ -318,13 +318,13 @@ resource "aws_iam_role" "monitoring_role" { name = "${local.prefix}-monitoring-role" assume_role_policy = jsonencode({ - "Version": "2012-10-17", - "Statement": [ + "Version" : "2012-10-17", + "Statement" : [ { - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" + "Effect" : "Allow", + "Action" : "sts:AssumeRole", + "Principal" : { + "Service" : "ecs-tasks.amazonaws.com" } }, { @@ -339,16 +339,16 @@ resource "aws_iam_role" "monitoring_role" { } resource "aws_iam_role_policy" "monitoring_policy" { - name = "${local.prefix}-monitoring-policy" - role = aws_iam_role.monitoring_role.id + name = "${local.prefix}-monitoring-policy" + role = aws_iam_role.monitoring_role.id policy = jsonencode({ - "Version": "2012-10-17", - "Statement": [ + "Version" : "2012-10-17", + "Statement" : [ { - "Sid": "AllowReadingMetricsFromCloudWatch", - "Effect": "Allow", - "Action": [ + "Sid" : "AllowReadingMetricsFromCloudWatch", + "Effect" : "Allow", + "Action" : [ "cloudwatch:DescribeAlarmsForMetric", "cloudwatch:DescribeAlarmHistory", "cloudwatch:DescribeAlarms", @@ -356,18 +356,18 @@ resource "aws_iam_role_policy" "monitoring_policy" { "cloudwatch:GetMetricData", "cloudwatch:GetInsightRuleReport" ], - "Resource": "*" + "Resource" : "*" }, { - "Sid": "AllowReadingResourceMetricsFromPerformanceInsights", - "Effect": "Allow", - "Action": "pi:GetResourceMetrics", - "Resource": "*" + "Sid" : "AllowReadingResourceMetricsFromPerformanceInsights", + "Effect" : "Allow", + "Action" : "pi:GetResourceMetrics", + "Resource" : "*" }, { - "Sid": "AllowReadingLogsFromCloudWatch", - "Effect": "Allow", - "Action": [ + "Sid" : "AllowReadingLogsFromCloudWatch", + "Effect" : "Allow", + "Action" : [ "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:GetLogEvents", @@ -377,23 +377,23 @@ resource "aws_iam_role_policy" "monitoring_policy" { "logs:StopQuery", "logs:GetQueryResults" ], - "Resource": "*" + "Resource" : "*" }, { - "Sid": "AllowReadingTagsInstancesRegionsFromEC2", - "Effect": "Allow", - "Action": [ + "Sid" : "AllowReadingTagsInstancesRegionsFromEC2", + "Effect" : "Allow", + "Action" : [ "ec2:DescribeTags", "ec2:DescribeInstances", "ec2:DescribeRegions" ], - "Resource": "*" + "Resource" : "*" }, { - "Sid": "AllowReadingResourcesForTags", - "Effect": "Allow", - "Action": "tag:GetResources", - "Resource": "*" + "Sid" : "AllowReadingResourcesForTags", + "Effect" : "Allow", + "Action" : "tag:GetResources", + "Resource" : "*" } ] }) @@ -405,69 +405,69 @@ resource "aws_iam_role_policy" "monitoring_policy" { data "aws_availability_zones" "available" {} resource "aws_vpc" "grafana_main" { - cidr_block = var.cidr_block - // enable dns resolution - enable_dns_support = true - enable_dns_hostnames = true - tags = { - Name = "${local.prefix}-vpc" - } + cidr_block = var.cidr_block + // enable dns resolution + enable_dns_support = true + enable_dns_hostnames = true + tags = { + Name = "${local.prefix}-vpc" + } } # Create var.az_count private subnets, each in a different AZ resource "aws_subnet" "grafana_private" { - count = var.az_count - cidr_block = cidrsubnet(aws_vpc.grafana_main.cidr_block, 8, count.index) - availability_zone = data.aws_availability_zones.available.names[count.index] - vpc_id = aws_vpc.grafana_main.id - tags = merge(var.tags, { - Name = "${local.prefix}-private-subnet-${count.index}" - }) + count = var.az_count + cidr_block = cidrsubnet(aws_vpc.grafana_main.cidr_block, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + vpc_id = aws_vpc.grafana_main.id + tags = merge(var.tags, { + Name = "${local.prefix}-private-subnet-${count.index}" + }) } # Create var.az_count public subnets, each in a different AZ resource "aws_subnet" "grafana_public" { - count = var.az_count - cidr_block = cidrsubnet(aws_vpc.grafana_main.cidr_block, 8, var.az_count + count.index) - availability_zone = data.aws_availability_zones.available.names[count.index] - vpc_id = aws_vpc.grafana_main.id - map_public_ip_on_launch = true - tags = merge(var.tags, { - Name = "${local.prefix}-public-subnet-${count.index}" - }) + count = var.az_count + cidr_block = cidrsubnet(aws_vpc.grafana_main.cidr_block, 8, var.az_count + count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + vpc_id = aws_vpc.grafana_main.id + map_public_ip_on_launch = true + tags = merge(var.tags, { + Name = "${local.prefix}-public-subnet-${count.index}" + }) } # Internet Gateway for the public subnet resource "aws_internet_gateway" "gw" { - vpc_id = aws_vpc.grafana_main.id - tags = merge(var.tags, { - Name = "${local.prefix}-igw" - }) + vpc_id = aws_vpc.grafana_main.id + tags = merge(var.tags, { + Name = "${local.prefix}-igw" + }) } # Route the public subnet traffic through the IGW resource "aws_route" "internet_access" { - route_table_id = aws_vpc.grafana_main.main_route_table_id - destination_cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.gw.id + route_table_id = aws_vpc.grafana_main.main_route_table_id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.gw.id } # Create a new route table for the private subnets resource "aws_route_table" "private" { - count = var.az_count - vpc_id = aws_vpc.grafana_main.id - tags = merge(var.tags, { - Name = "${local.prefix}-private-rt-${count.index}" - }) + count = var.az_count + vpc_id = aws_vpc.grafana_main.id + tags = merge(var.tags, { + Name = "${local.prefix}-private-rt-${count.index}" + }) } # Route the private subnet traffic through the NAT Gateway resource "aws_route" "private_nat_gateway" { - count = var.az_count - route_table_id = element(aws_route_table.private[*].id, count.index) + count = var.az_count + route_table_id = element(aws_route_table.private[*].id, count.index) destination_cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.nat.id } @@ -475,9 +475,9 @@ resource "aws_route" "private_nat_gateway" { # Explicitly associate the newly created route tables to the private subnets (so they don't default to the main route table) resource "aws_route_table_association" "private" { - count = var.az_count - subnet_id = element(aws_subnet.grafana_private[*].id, count.index) - route_table_id = element(aws_route_table.private[*].id, count.index) + count = var.az_count + subnet_id = element(aws_subnet.grafana_private[*].id, count.index) + route_table_id = element(aws_route_table.private[*].id, count.index) } @@ -544,7 +544,7 @@ resource "aws_eip" "nat" { resource "aws_nat_gateway" "nat" { allocation_id = aws_eip.nat.id - subnet_id = element(aws_subnet.grafana_public[*].id, 0) + subnet_id = element(aws_subnet.grafana_public[*].id, 0) tags = merge(var.tags, { Name = "${local.prefix}-nat-gw" }) diff --git a/grafana/non-prod/terraform/logs.tf b/grafana/non-prod/terraform/logs.tf index 8716e28cb..ebabb7d4a 100644 --- a/grafana/non-prod/terraform/logs.tf +++ b/grafana/non-prod/terraform/logs.tf @@ -4,15 +4,15 @@ # Set up CloudWatch group and log stream and retain logs for 30 days resource "aws_cloudwatch_log_group" "grafana_log_group" { - name = local.log_group + name = local.log_group retention_in_days = 30 tags = merge(var.tags, { - Name = local.log_group + Name = local.log_group }) } resource "aws_cloudwatch_log_stream" "grafana_log_group" { - name = "${local.log_group}-stream" + name = "${local.log_group}-stream" log_group_name = aws_cloudwatch_log_group.grafana_log_group.name } diff --git a/grafana/non-prod/terraform/output.tf b/grafana/non-prod/terraform/output.tf index a0e0b69e6..4648dca7b 100644 --- a/grafana/non-prod/terraform/output.tf +++ b/grafana/non-prod/terraform/output.tf @@ -52,18 +52,18 @@ output "alb_listener_arn" { } output "prefix" { - value = local.prefix + value = local.prefix } output "app_image" { - value = local.app_image + value = local.app_image } output "app_name" { - value = local.app_name + value = local.app_name } output "Monitoring_Role_Arn" { - value = aws_iam_role.monitoring_role.arn + value = aws_iam_role.monitoring_role.arn } diff --git a/grafana/non-prod/terraform/terraform.tfvars b/grafana/non-prod/terraform/terraform.tfvars index 027f5a09d..2e7bf5719 100644 --- a/grafana/non-prod/terraform/terraform.tfvars +++ b/grafana/non-prod/terraform/terraform.tfvars @@ -1,13 +1,13 @@ -project_name = "immunisations" +project_name = "immunisations" project_short_name = "imms" -service = "grafana" -aws_region = "eu-west-2" -az_count = 2 -app_port = 3000 -app_count = 1 -health_check_path = "/api/health" -fargate_cpu = 1024 -fargate_memory = 2048 -cidr_block = "10.0.0.0/16" -app_version = "11.0.0-22.04_stable" -use_natgw = true +service = "grafana" +aws_region = "eu-west-2" +az_count = 2 +app_port = 3000 +app_count = 1 +health_check_path = "/api/health" +fargate_cpu = 1024 +fargate_memory = 2048 +cidr_block = "10.0.0.0/16" +app_version = "11.0.0-22.04_stable" +use_natgw = true diff --git a/grafana/non-prod/terraform/variables.tf b/grafana/non-prod/terraform/variables.tf index 72aa4dc7d..49c014b9f 100644 --- a/grafana/non-prod/terraform/variables.tf +++ b/grafana/non-prod/terraform/variables.tf @@ -1,78 +1,78 @@ variable "project_name" { - default = "immunisations" + default = "immunisations" } variable "project_short_name" { - default = "imms" + default = "imms" } variable "service" { - default = "fhir-graf" + default = "fhir-graf" } variable "aws_region" { - description = "Destination AWS region" + description = "Destination AWS region" } variable "az_count" { - description = "Number of AZs to cover in a given region" - default = 2 + description = "Number of AZs to cover in a given region" + default = 2 } variable "app_version" { - description = "Version of the Docker image to run in the ECS cluster" - default = "11.0.0-22.04_stable" + description = "Version of the Docker image to run in the ECS cluster" + default = "11.0.0-22.04_stable" } variable "app_port" { - description = "Port exposed by the docker image to redirect traffic to" + description = "Port exposed by the docker image to redirect traffic to" } variable "app_count" { - description = "Number of docker containers to run" + description = "Number of docker containers to run" } variable "health_check_path" { - description = "Health check path for the ALB" + description = "Health check path for the ALB" } variable "fargate_cpu" { - description = "Fargate instance CPU units to provision (1 vCPU = 1024 CPU units)" + description = "Fargate instance CPU units to provision (1 vCPU = 1024 CPU units)" } variable "fargate_memory" { - description = "Fargate instance memory to provision (in MiB)" + description = "Fargate instance memory to provision (in MiB)" } variable "cidr_block" { - description = "CIDR block for the VPC" + description = "CIDR block for the VPC" } variable "use_natgw" { - description = "Boolean to determine whether to use the NAT Gateway module" - type = bool - default = true + description = "Boolean to determine whether to use the NAT Gateway module" + type = bool + default = true } variable "tags" { - description = "A map of tags to add to all resources" - type = map(string) - default = {} + description = "A map of tags to add to all resources" + type = map(string) + default = {} } locals { - environment = terraform.workspace == "green" ? "prod" : terraform.workspace == "blue" ? "prod" : terraform.workspace - env = terraform.workspace - prefix = "${var.project_short_name}-${local.env}-${var.service}" - - account_id = data.aws_caller_identity.current.account_id - app_image = "${local.account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/${local.prefix}-app:${var.app_version}" - app_name = "${local.prefix}-app" - log_group = "${local.prefix}-log" - - tags = { - Environment = terraform.workspace - Project = local.prefix - } + environment = terraform.workspace == "green" ? "prod" : terraform.workspace == "blue" ? "prod" : terraform.workspace + env = terraform.workspace + prefix = "${var.project_short_name}-${local.env}-${var.service}" + + account_id = data.aws_caller_identity.current.account_id + app_image = "${local.account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/${local.prefix}-app:${var.app_version}" + app_name = "${local.prefix}-app" + log_group = "${local.prefix}-log" + + tags = { + Environment = terraform.workspace + Project = local.prefix + } } diff --git a/immunisation-fhir-api.code-workspace b/immunisation-fhir-api.code-workspace index 70e325df8..2effca143 100644 --- a/immunisation-fhir-api.code-workspace +++ b/immunisation-fhir-api.code-workspace @@ -1,44 +1,44 @@ { - "folders": [ - { - "path": "." - }, - { - "path": "backend" - }, - { - "path": "filenameprocessor" - }, - { - "path": "recordprocessor" - }, - { - "path": "delta_backend" - }, - { - "path": "mesh_processor" - }, - { - "path": "e2e" - }, - { - "path": "e2e_batch" - }, - { - "path": "lambdas/ack_backend" - }, - { - "path": "lambdas/redis_sync" - }, - { - "path": "lambdas/id_sync" - }, - { - "path": "lambdas/mns_subscription" - }, - { - "path": "lambdas/shared" - } - ], - "settings": {} + "folders": [ + { + "path": ".", + }, + { + "path": "backend", + }, + { + "path": "filenameprocessor", + }, + { + "path": "recordprocessor", + }, + { + "path": "delta_backend", + }, + { + "path": "mesh_processor", + }, + { + "path": "e2e", + }, + { + "path": "e2e_batch", + }, + { + "path": "lambdas/ack_backend", + }, + { + "path": "lambdas/redis_sync", + }, + { + "path": "lambdas/id_sync", + }, + { + "path": "lambdas/mns_subscription", + }, + { + "path": "lambdas/shared", + }, + ], + "settings": {}, } diff --git a/infra/README.MD b/infra/README.MD index 1736ca278..d6a970584 100644 --- a/infra/README.MD +++ b/infra/README.MD @@ -1,18 +1,23 @@ -# About +# About + Use .env-default as a reference for the required environment variables. You can use the commands defined in the Makefile to interact with the infrastructure resources. Currently, this process is run manually whenever we need to update the base layer of our infrastructure. These core resources remain consistent across all deployments. ## Running terraform -The general procedures are: + +The general procedures are: + 1. Set up your environment by creating a .env file with the following values. Note: some values may require customisation based on your specific setup. + ```dotenv ENVIRONMENT=(select subfolder from environments) AWS_PROFILE=your-profile BUCKET_NAME=(find bucket name in aws) TF_VAR_key=state ``` + 2. Run `make init` to initialize the Terraform project. 3. Run `make plan` to review the proposed infrastructure changes. -4. Once you're confident in the plan and understand its impact, execute `make apply` to apply the changes. \ No newline at end of file +4. Once you're confident in the plan and understand its impact, execute `make apply` to apply the changes. diff --git a/infra/auto_ops_policy.json b/infra/auto_ops_policy.json index 23e8df535..a3ae6e12e 100644 --- a/infra/auto_ops_policy.json +++ b/infra/auto_ops_policy.json @@ -1,223 +1,223 @@ { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": [ - "iam:CreateServiceSpecificCredential", - "firehose:*", - "iam:TagMFADevice", - "iam:ListServiceSpecificCredentials", - "iam:PutRolePolicy", - "iam:ListSigningCertificates", - "iam:AddRoleToInstanceProfile", - "ses:SendEmail", - "iam:SimulateCustomPolicy", - "iam:ListRolePolicies", - "iam:DeleteOpenIDConnectProvider", - "iam:PutGroupPolicy", - "iam:ListPolicies", - "sns:*", - "iam:GetRole", - "iam:ListSAMLProviders", - "apigateway:*", - "iam:TagPolicy", - "iam:UpdateServerCertificate", - "cloudwatch:*", - "pipes:*", - "ecs:*", - "ec2:*", - "iam:GetOpenIDConnectProvider", - "iam:UntagRole", - "iam:PutRolePermissionsBoundary", - "iam:TagRole", - "cloudtrail:*", - "iam:ResetServiceSpecificCredential", - "iam:DeleteRolePermissionsBoundary", - "iam:ListInstanceProfilesForRole", - "iam:PassRole", - "iam:DeleteRolePolicy", - "kms:*", - "iam:EnableMFADevice", - "iam:ResyncMFADevice", - "iam:ListCloudFrontPublicKeys", - "guardduty:*", - "iam:ListRoles", - "iam:DeleteUser", - "iam:GetContextKeysForCustomPolicy", - "iam:CreatePolicy", - "iam:CreateServiceLinkedRole", - "iam:AttachGroupPolicy", - "iam:DeleteVirtualMFADevice", - "ecr:*", - "iam:UpdateRole", - "iam:UntagOpenIDConnectProvider", - "iam:ListGroups", - "iam:UntagInstanceProfile", - "iam:DeleteServiceSpecificCredential", - "iam:TagOpenIDConnectProvider", - "iam:DeleteSAMLProvider", - "iam:UpdateAssumeRolePolicy", - "iam:GetPolicyVersion", - "application-autoscaling:*", - "iam:DeleteGroup", - "iam:GetMFADevice", - "iam:ListServerCertificates", - "iam:RemoveRoleFromInstanceProfile", - "iam:UpdateGroup", - "dynamodb:*", - "iam:ListVirtualMFADevices", - "servicediscovery:*", - "cloudfront:*", - "iam:ListSSHPublicKeys", - "iam:GetAccountEmailAddress", - "iam:ListOpenIDConnectProviderTags", - "config:*", - "ebs:*", - "iam:DeleteCloudFrontPublicKey", - "events:*", - "iam:ChangePassword", - "iam:UpdateLoginProfile", - "iam:GetServerCertificate", - "iam:GetAccessKeyLastUsed", - "iam:UpdateSSHPublicKey", - "iam:UpdateAccountPasswordPolicy", - "iam:DeleteServiceLinkedRole", - "iam:ListSTSRegionalEndpointsStatus", - "iam:GetAccountSummary", - "iam:DeletePolicy", - "iam:CreateVirtualMFADevice", - "iam:ListMFADevices", - "iam:AddUserToGroup", - "tag:*", - "iam:CreatePolicyVersion", - "iam:GetInstanceProfile", - "elasticloadbalancing:*", - "iam:UntagServerCertificate", - "iam:ListUserPolicies", - "iam:TagUser", - "iam:ListPolicyVersions", - "iam:ListOpenIDConnectProviders", - "lambda:*", - "iam:ListUsers", - "iam:UpdateSigningCertificate", - "iam:ListUserTags", - "iam:GetAccountPasswordPolicy", - "iam:DeactivateMFADevice", - "iam:DeleteAccessKey", - "rds:*", - "iam:ListRoleTags", - "iam:UpdateCloudFrontPublicKey", - "iam:GenerateServiceLastAccessedDetails", - "iam:UpdateOpenIDConnectProviderThumbprint", - "iam:SetSecurityTokenServicePreferences", - "iam:DeleteServerCertificate", - "quicksight:*", - "iam:UploadSSHPublicKey", - "iam:DetachGroupPolicy", - "iam:GetCredentialReport", - "iam:UpdateServiceSpecificCredential", - "iam:GetPolicy", - "iam:RemoveClientIDFromOpenIDConnectProvider", - "iam:ListEntitiesForPolicy", - "iam:DeleteRole", - "iam:UpdateRoleDescription", - "iam:UploadCloudFrontPublicKey", - "iam:GetRolePolicy", - "iam:CreateInstanceProfile", - "iam:GenerateCredentialReport", - "sqs:*", - "iam:GetServiceLastAccessedDetails", - "athena:*", - "iam:GetServiceLinkedRoleDeletionStatus", - "iam:ListAttachedGroupPolicies", - "iam:ListPolicyTags", - "iam:DeleteAccountAlias", - "iam:UpdateSAMLProvider", - "iam:ListAccessKeys", - "iam:DeleteInstanceProfile", - "elasticfilesystem:*", - "cognito-identity:*", - "s3:*", - "iam:ListGroupPolicies", - "ses:SendRawEmail", - "iam:GetSSHPublicKey", - "iam:PutUserPermissionsBoundary", - "iam:DeleteUserPermissionsBoundary", - "ssm:*", - "iam:ListServerCertificateTags", - "iam:PutUserPolicy", - "iam:TagServerCertificate", - "iam:ListAccountAliases", - "iam:UntagPolicy", - "iam:GetUser", - "iam:GetLoginProfile", - "acm:*", - "iam:TagInstanceProfile", - "iam:SetDefaultPolicyVersion", - "logs:*", - "iam:CreateRole", - "iam:AttachRolePolicy", - "iam:SetSTSRegionalEndpointStatus", - "iam:TagSAMLProvider", - "autoscaling:*", - "iam:CreateLoginProfile", - "iam:DetachRolePolicy", - "iam:SimulatePrincipalPolicy", - "secretsmanager:*", - "iam:ListAttachedRolePolicies", - "iam:CreateAccountAlias", - "iam:ListSAMLProviderTags", - "kinesis:*", - "iam:DetachUserPolicy", - "iam:GetAccountAuthorizationDetails", - "iam:CreateGroup", - "iam:UntagSAMLProvider", - "iam:UpdateUser", - "iam:DeleteUserPolicy", - "iam:AttachUserPolicy", - "iam:UpdateAccessKey", - "iam:DeleteSigningCertificate", - "iam:GetUserPolicy", - "waf:*", - "iam:ListGroupsForUser", - "iam:GetAccountName", - "cognito-idp:*", - "iam:GetGroupPolicy", - "iam:GetServiceLastAccessedDetailsWithEntities", - "iam:ListPoliciesGrantingServiceAccess", - "iam:DeleteSSHPublicKey", - "iam:ListInstanceProfileTags", - "iam:CreateUser", - "iam:GetGroup", - "glue:*", - "iam:GetOrganizationsAccessReport", - "iam:CreateAccessKey", - "iam:GetContextKeysForPrincipalPolicy", - "iam:UpdateAccountName", - "iam:RemoveUserFromGroup", - "wafv2:*", - "iam:GetCloudFrontPublicKey", - "iam:ListAttachedUserPolicies", - "iam:UpdateAccountEmailAddress", - "iam:GetSAMLProvider", - "iam:DeleteLoginProfile", - "iam:UploadSigningCertificate", - "iam:DeleteAccountPasswordPolicy", - "iam:ListInstanceProfiles", - "iam:CreateOpenIDConnectProvider", - "iam:UploadServerCertificate", - "iam:UntagUser", - "iam:UntagMFADevice", - "route53:*", - "iam:DeleteGroupPolicy", - "iam:ListMFADeviceTags", - "elasticache:*", - "iam:DeletePolicyVersion", - "chatbot:*" - ], - "Resource": "*" - } - ] -} \ No newline at end of file + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "iam:CreateServiceSpecificCredential", + "firehose:*", + "iam:TagMFADevice", + "iam:ListServiceSpecificCredentials", + "iam:PutRolePolicy", + "iam:ListSigningCertificates", + "iam:AddRoleToInstanceProfile", + "ses:SendEmail", + "iam:SimulateCustomPolicy", + "iam:ListRolePolicies", + "iam:DeleteOpenIDConnectProvider", + "iam:PutGroupPolicy", + "iam:ListPolicies", + "sns:*", + "iam:GetRole", + "iam:ListSAMLProviders", + "apigateway:*", + "iam:TagPolicy", + "iam:UpdateServerCertificate", + "cloudwatch:*", + "pipes:*", + "ecs:*", + "ec2:*", + "iam:GetOpenIDConnectProvider", + "iam:UntagRole", + "iam:PutRolePermissionsBoundary", + "iam:TagRole", + "cloudtrail:*", + "iam:ResetServiceSpecificCredential", + "iam:DeleteRolePermissionsBoundary", + "iam:ListInstanceProfilesForRole", + "iam:PassRole", + "iam:DeleteRolePolicy", + "kms:*", + "iam:EnableMFADevice", + "iam:ResyncMFADevice", + "iam:ListCloudFrontPublicKeys", + "guardduty:*", + "iam:ListRoles", + "iam:DeleteUser", + "iam:GetContextKeysForCustomPolicy", + "iam:CreatePolicy", + "iam:CreateServiceLinkedRole", + "iam:AttachGroupPolicy", + "iam:DeleteVirtualMFADevice", + "ecr:*", + "iam:UpdateRole", + "iam:UntagOpenIDConnectProvider", + "iam:ListGroups", + "iam:UntagInstanceProfile", + "iam:DeleteServiceSpecificCredential", + "iam:TagOpenIDConnectProvider", + "iam:DeleteSAMLProvider", + "iam:UpdateAssumeRolePolicy", + "iam:GetPolicyVersion", + "application-autoscaling:*", + "iam:DeleteGroup", + "iam:GetMFADevice", + "iam:ListServerCertificates", + "iam:RemoveRoleFromInstanceProfile", + "iam:UpdateGroup", + "dynamodb:*", + "iam:ListVirtualMFADevices", + "servicediscovery:*", + "cloudfront:*", + "iam:ListSSHPublicKeys", + "iam:GetAccountEmailAddress", + "iam:ListOpenIDConnectProviderTags", + "config:*", + "ebs:*", + "iam:DeleteCloudFrontPublicKey", + "events:*", + "iam:ChangePassword", + "iam:UpdateLoginProfile", + "iam:GetServerCertificate", + "iam:GetAccessKeyLastUsed", + "iam:UpdateSSHPublicKey", + "iam:UpdateAccountPasswordPolicy", + "iam:DeleteServiceLinkedRole", + "iam:ListSTSRegionalEndpointsStatus", + "iam:GetAccountSummary", + "iam:DeletePolicy", + "iam:CreateVirtualMFADevice", + "iam:ListMFADevices", + "iam:AddUserToGroup", + "tag:*", + "iam:CreatePolicyVersion", + "iam:GetInstanceProfile", + "elasticloadbalancing:*", + "iam:UntagServerCertificate", + "iam:ListUserPolicies", + "iam:TagUser", + "iam:ListPolicyVersions", + "iam:ListOpenIDConnectProviders", + "lambda:*", + "iam:ListUsers", + "iam:UpdateSigningCertificate", + "iam:ListUserTags", + "iam:GetAccountPasswordPolicy", + "iam:DeactivateMFADevice", + "iam:DeleteAccessKey", + "rds:*", + "iam:ListRoleTags", + "iam:UpdateCloudFrontPublicKey", + "iam:GenerateServiceLastAccessedDetails", + "iam:UpdateOpenIDConnectProviderThumbprint", + "iam:SetSecurityTokenServicePreferences", + "iam:DeleteServerCertificate", + "quicksight:*", + "iam:UploadSSHPublicKey", + "iam:DetachGroupPolicy", + "iam:GetCredentialReport", + "iam:UpdateServiceSpecificCredential", + "iam:GetPolicy", + "iam:RemoveClientIDFromOpenIDConnectProvider", + "iam:ListEntitiesForPolicy", + "iam:DeleteRole", + "iam:UpdateRoleDescription", + "iam:UploadCloudFrontPublicKey", + "iam:GetRolePolicy", + "iam:CreateInstanceProfile", + "iam:GenerateCredentialReport", + "sqs:*", + "iam:GetServiceLastAccessedDetails", + "athena:*", + "iam:GetServiceLinkedRoleDeletionStatus", + "iam:ListAttachedGroupPolicies", + "iam:ListPolicyTags", + "iam:DeleteAccountAlias", + "iam:UpdateSAMLProvider", + "iam:ListAccessKeys", + "iam:DeleteInstanceProfile", + "elasticfilesystem:*", + "cognito-identity:*", + "s3:*", + "iam:ListGroupPolicies", + "ses:SendRawEmail", + "iam:GetSSHPublicKey", + "iam:PutUserPermissionsBoundary", + "iam:DeleteUserPermissionsBoundary", + "ssm:*", + "iam:ListServerCertificateTags", + "iam:PutUserPolicy", + "iam:TagServerCertificate", + "iam:ListAccountAliases", + "iam:UntagPolicy", + "iam:GetUser", + "iam:GetLoginProfile", + "acm:*", + "iam:TagInstanceProfile", + "iam:SetDefaultPolicyVersion", + "logs:*", + "iam:CreateRole", + "iam:AttachRolePolicy", + "iam:SetSTSRegionalEndpointStatus", + "iam:TagSAMLProvider", + "autoscaling:*", + "iam:CreateLoginProfile", + "iam:DetachRolePolicy", + "iam:SimulatePrincipalPolicy", + "secretsmanager:*", + "iam:ListAttachedRolePolicies", + "iam:CreateAccountAlias", + "iam:ListSAMLProviderTags", + "kinesis:*", + "iam:DetachUserPolicy", + "iam:GetAccountAuthorizationDetails", + "iam:CreateGroup", + "iam:UntagSAMLProvider", + "iam:UpdateUser", + "iam:DeleteUserPolicy", + "iam:AttachUserPolicy", + "iam:UpdateAccessKey", + "iam:DeleteSigningCertificate", + "iam:GetUserPolicy", + "waf:*", + "iam:ListGroupsForUser", + "iam:GetAccountName", + "cognito-idp:*", + "iam:GetGroupPolicy", + "iam:GetServiceLastAccessedDetailsWithEntities", + "iam:ListPoliciesGrantingServiceAccess", + "iam:DeleteSSHPublicKey", + "iam:ListInstanceProfileTags", + "iam:CreateUser", + "iam:GetGroup", + "glue:*", + "iam:GetOrganizationsAccessReport", + "iam:CreateAccessKey", + "iam:GetContextKeysForPrincipalPolicy", + "iam:UpdateAccountName", + "iam:RemoveUserFromGroup", + "wafv2:*", + "iam:GetCloudFrontPublicKey", + "iam:ListAttachedUserPolicies", + "iam:UpdateAccountEmailAddress", + "iam:GetSAMLProvider", + "iam:DeleteLoginProfile", + "iam:UploadSigningCertificate", + "iam:DeleteAccountPasswordPolicy", + "iam:ListInstanceProfiles", + "iam:CreateOpenIDConnectProvider", + "iam:UploadServerCertificate", + "iam:UntagUser", + "iam:UntagMFADevice", + "route53:*", + "iam:DeleteGroupPolicy", + "iam:ListMFADeviceTags", + "elasticache:*", + "iam:DeletePolicyVersion", + "chatbot:*" + ], + "Resource": "*" + } + ] +} diff --git a/infra/batch_processor_errors_slack_chatbot.tf b/infra/batch_processor_errors_slack_chatbot.tf index e28683eb4..db521c081 100644 --- a/infra/batch_processor_errors_slack_chatbot.tf +++ b/infra/batch_processor_errors_slack_chatbot.tf @@ -7,7 +7,7 @@ resource "aws_chatbot_slack_channel_configuration" "batch_processor_errors" { } resource "aws_iam_role" "batch_processor_errors_chatbot" { - name = "${var.environment}-batch-processor-errors-chatbot-channel-role" + name = "${var.environment}-batch-processor-errors-chatbot-channel-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ diff --git a/infra/batch_processor_errors_sns_topic.tf b/infra/batch_processor_errors_sns_topic.tf index 534afffbf..48e16b0e1 100644 --- a/infra/batch_processor_errors_sns_topic.tf +++ b/infra/batch_processor_errors_sns_topic.tf @@ -4,18 +4,18 @@ resource "aws_sns_topic" "batch_processor_errors" { } resource "aws_sns_topic_policy" "batch_processor_errors_topic_policy" { - arn = aws_sns_topic.batch_processor_errors.arn + arn = aws_sns_topic.batch_processor_errors.arn policy = jsonencode({ Version = "2012-10-17", Statement = [ { - Sid = "AllowCloudWatchToPublish", - Effect = "Allow", + Sid = "AllowCloudWatchToPublish", + Effect = "Allow", Principal = { Service = "cloudwatch.amazonaws.com" }, - Action = "SNS:Publish", - Resource = aws_sns_topic.batch_processor_errors.arn + Action = "SNS:Publish", + Resource = aws_sns_topic.batch_processor_errors.arn } ] }) diff --git a/infra/csoc_eventforwarder_role.tf b/infra/csoc_eventforwarder_role.tf index b1252e98f..fc9ad6de6 100644 --- a/infra/csoc_eventforwarder_role.tf +++ b/infra/csoc_eventforwarder_role.tf @@ -3,10 +3,10 @@ resource "aws_iam_role" "eventbridge_forwarder_role" { assume_role_policy = jsonencode({ Version : "2012-10-17", Statement = [{ - Sid = "TrustEventBridgeService", - Effect = "Allow", + Sid = "TrustEventBridgeService", + Effect = "Allow", Principal = { Service = "events.amazonaws.com" }, - Action = "sts:AssumeRole", + Action = "sts:AssumeRole", Condition = { StringEquals = { "aws:SourceAccount" = var.imms_account_id diff --git a/infra/iam.tf b/infra/iam.tf index b0d78a1c0..e10f73eae 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -69,10 +69,10 @@ resource "aws_iam_role" "auto_ops" { Action = "sts:AssumeRoleWithWebIdentity", Condition = { StringEquals = { - "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com" }, StringLike = { - "token.actions.githubusercontent.com:sub": var.environment != "prod" ? [ + "token.actions.githubusercontent.com:sub" : var.environment != "prod" ? [ "repo:NHSDigital/immunisation-fhir-api:*", "repo:NHSDigital/imms_fhir_api_automation:*" ] : ["repo:NHSDigital/immunisation-fhir-api:*"] diff --git a/infra/kms.tf b/infra/kms.tf index 020fb5bf5..b20428381 100644 --- a/infra/kms.tf +++ b/infra/kms.tf @@ -73,7 +73,7 @@ locals { Principal = { AWS = "arn:aws:iam::${var.mns_account_id}:${var.mns_admin_role}" }, - Action = "kms:GenerateDataKey", + Action = "kms:GenerateDataKey", Resource = "*" } } @@ -199,17 +199,17 @@ resource "aws_kms_key" "batch_processor_errors_sns_encryption_key" { { Effect = "Allow", Principal = { - "Service": "cloudwatch.amazonaws.com" + "Service" : "cloudwatch.amazonaws.com" }, - Action = ["kms:GenerateDataKey*", "kms:Decrypt"], + Action = ["kms:GenerateDataKey*", "kms:Decrypt"], Resource = "*" }, { Effect = "Allow", Principal = { - "Service": "chatbot.amazonaws.com" + "Service" : "chatbot.amazonaws.com" }, - Action = ["kms:GenerateDataKey*", "kms:Decrypt"], + Action = ["kms:GenerateDataKey*", "kms:Decrypt"], Resource = "*" } ] diff --git a/infra/shield_protection.tf b/infra/shield_protection.tf index 04bd70d6f..4b0ed706c 100644 --- a/infra/shield_protection.tf +++ b/infra/shield_protection.tf @@ -30,7 +30,7 @@ locals { } global_shield_arn = { route53_parent_zone = aws_shield_protection.parent_dns.resource_arn - route53_child_zone = aws_shield_protection.child_dns.resource_arn + route53_child_zone = aws_shield_protection.child_dns.resource_arn } } @@ -39,8 +39,8 @@ locals { resource "aws_cloudwatch_metric_alarm" "ddos_protection_regional" { for_each = local.regional_shield_arn - alarm_name = "imms-${var.environment}-shield_ddos_${each.key}" - alarm_description = "Alarm when Shield detects DDoS on ${each.key}" + alarm_name = "imms-${var.environment}-shield_ddos_${each.key}" + alarm_description = "Alarm when Shield detects DDoS on ${each.key}" namespace = "AWS/DDoSProtection" metric_name = "DDoSDetected" @@ -61,9 +61,9 @@ resource "aws_cloudwatch_metric_alarm" "ddos_protection_regional" { resource "aws_cloudwatch_metric_alarm" "ddos_protection_global" { for_each = local.global_shield_arn - provider = aws.use1 - alarm_name = "imms-${var.environment}-shield_ddos_${each.key}" - alarm_description = "Alarm when Shield detects DDoS on ${each.key}" + provider = aws.use1 + alarm_name = "imms-${var.environment}-shield_ddos_${each.key}" + alarm_description = "Alarm when Shield detects DDoS on ${each.key}" namespace = "AWS/DDoSProtection" metric_name = "DDoSDetected" @@ -90,7 +90,7 @@ resource "aws_cloudwatch_event_rule" "shield_ddos_rule_regional" { event_pattern = jsonencode({ "source" = ["aws.cloudwatch"], "detail-type" = ["CloudWatch Alarm State Change"], - "resources" = [ + "resources" = [ for alarm in aws_cloudwatch_metric_alarm.ddos_protection_regional : alarm.arn ] }) @@ -115,7 +115,7 @@ resource "aws_cloudwatch_event_rule" "shield_ddos_rule_global" { event_pattern = jsonencode({ "source" = ["aws.cloudwatch"], "detail-type" = ["CloudWatch Alarm State Change"], - "resources" = [ + "resources" = [ for alarm in aws_cloudwatch_metric_alarm.ddos_protection_global : alarm.arn ] }) diff --git a/lambdas/ack_backend/src/ack_processor.py b/lambdas/ack_backend/src/ack_processor.py index 8ecb9bb4e..de84fee57 100644 --- a/lambdas/ack_backend/src/ack_processor.py +++ b/lambdas/ack_backend/src/ack_processor.py @@ -44,6 +44,16 @@ def lambda_handler(event, context): for message in incoming_message_body: ack_data_rows.append(convert_message_to_ack_row(message, created_at_formatted_string)) - update_ack_file(file_key, message_id, supplier, vaccine_type, created_at_formatted_string, ack_data_rows) - - return {"statusCode": 200, "body": json.dumps("Lambda function executed successfully!")} + update_ack_file( + file_key, + message_id, + supplier, + vaccine_type, + created_at_formatted_string, + ack_data_rows, + ) + + return { + "statusCode": 200, + "body": json.dumps("Lambda function executed successfully!"), + } diff --git a/lambdas/ack_backend/src/logging_decorators.py b/lambdas/ack_backend/src/logging_decorators.py index 34fbca92b..9327c9e9e 100644 --- a/lambdas/ack_backend/src/logging_decorators.py +++ b/lambdas/ack_backend/src/logging_decorators.py @@ -15,7 +15,10 @@ def convert_message_to_ack_row_logging_decorator(func): @wraps(func) def wrapper(message, created_at_formatted_string): - base_log_data = {"function_name": f"{PREFIX}_{func.__name__}", "date_time": str(datetime.now())} + base_log_data = { + "function_name": f"{PREFIX}_{func.__name__}", + "date_time": str(datetime.now()), + } start_time = time.time() try: @@ -36,14 +39,30 @@ def wrapper(message, created_at_formatted_string): "operation_requested": message.get("operation_requested", "unknown"), **process_diagnostics(diagnostics, file_key, message_id), } - generate_and_send_logs(STREAM_NAME, start_time, base_log_data, additional_log_data, use_ms_precision=True) + generate_and_send_logs( + STREAM_NAME, + start_time, + base_log_data, + additional_log_data, + use_ms_precision=True, + ) return result except Exception as error: - additional_log_data = {"status": "fail", "statusCode": 500, "diagnostics": str(error)} - generate_and_send_logs(STREAM_NAME, start_time, base_log_data, additional_log_data, use_ms_precision=True, - is_error_log=True) + additional_log_data = { + "status": "fail", + "statusCode": 500, + "diagnostics": str(error), + } + generate_and_send_logs( + STREAM_NAME, + start_time, + base_log_data, + additional_log_data, + use_ms_precision=True, + is_error_log=True, + ) raise return wrapper @@ -55,7 +74,10 @@ def upload_ack_file_logging_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - base_log_data = {"function_name": f"{PREFIX}_{func.__name__}", "date_time": str(datetime.now())} + base_log_data = { + "function_name": f"{PREFIX}_{func.__name__}", + "date_time": str(datetime.now()), + } start_time = time.time() # NB this doesn't require a try-catch block as the wrapped function never throws an exception @@ -63,7 +85,11 @@ def wrapper(*args, **kwargs): if result is not None: message_for_logs = "Record processing complete" base_log_data.update(result) - additional_log_data = {"status": "success", "statusCode": 200, "message": message_for_logs} + additional_log_data = { + "status": "success", + "statusCode": 200, + "message": message_for_logs, + } generate_and_send_logs(STREAM_NAME, start_time, base_log_data, additional_log_data) return result @@ -76,19 +102,36 @@ def ack_lambda_handler_logging_decorator(func): @wraps(func) def wrapper(event, context, *args, **kwargs): - base_log_data = {"function_name": f"{PREFIX}_{func.__name__}", "date_time": str(datetime.now())} + base_log_data = { + "function_name": f"{PREFIX}_{func.__name__}", + "date_time": str(datetime.now()), + } start_time = time.time() try: result = func(event, context, *args, **kwargs) message_for_logs = "Lambda function executed successfully!" - additional_log_data = {"status": "success", "statusCode": 200, "message": message_for_logs} + additional_log_data = { + "status": "success", + "statusCode": 200, + "message": message_for_logs, + } generate_and_send_logs(STREAM_NAME, start_time, base_log_data, additional_log_data) return result except Exception as error: - additional_log_data = {"status": "fail", "statusCode": 500, "diagnostics": str(error)} - generate_and_send_logs(STREAM_NAME, start_time, base_log_data, additional_log_data, is_error_log=True) + additional_log_data = { + "status": "fail", + "statusCode": 500, + "diagnostics": str(error), + } + generate_and_send_logs( + STREAM_NAME, + start_time, + base_log_data, + additional_log_data, + is_error_log=True, + ) raise return wrapper @@ -99,7 +142,7 @@ def process_diagnostics(diagnostics, file_key, message_id): if diagnostics is not None: return { "status": "fail", - "statusCode": diagnostics.get("statusCode") if isinstance(diagnostics, dict) else 500, + "statusCode": (diagnostics.get("statusCode") if isinstance(diagnostics, dict) else 500), "diagnostics": ( diagnostics.get("error_message") if isinstance(diagnostics, dict) @@ -111,4 +154,8 @@ def process_diagnostics(diagnostics, file_key, message_id): diagnostics = "An unhandled error occurred during batch processing" return {"status": "fail", "statusCode": 500, "diagnostics": diagnostics} - return {"status": "success", "statusCode": 200, "diagnostics": "Operation completed successfully"} + return { + "status": "success", + "statusCode": 200, + "diagnostics": "Operation completed successfully", + } diff --git a/lambdas/ack_backend/src/update_ack_file.py b/lambdas/ack_backend/src/update_ack_file.py index 85d6471d4..3ae60dd86 100644 --- a/lambdas/ack_backend/src/update_ack_file.py +++ b/lambdas/ack_backend/src/update_ack_file.py @@ -141,7 +141,9 @@ def move_file(bucket_name: str, source_file_key: str, destination_file_key: str) """Moves a file from one location to another within a single S3 bucket by copying and then deleting the file.""" s3_client = get_s3_client() s3_client.copy_object( - Bucket=bucket_name, CopySource={"Bucket": bucket_name, "Key": source_file_key}, Key=destination_file_key + Bucket=bucket_name, + CopySource={"Bucket": bucket_name, "Key": source_file_key}, + Key=destination_file_key, ) s3_client.delete_object(Bucket=bucket_name, Key=source_file_key) logger.info("File moved from %s to %s", source_file_key, destination_file_key) diff --git a/lambdas/ack_backend/tests/test_ack_processor.py b/lambdas/ack_backend/tests/test_ack_processor.py index 382cebcc5..82e6a3262 100644 --- a/lambdas/ack_backend/tests/test_ack_processor.py +++ b/lambdas/ack_backend/tests/test_ack_processor.py @@ -8,8 +8,15 @@ from boto3 import client as boto3_client from moto import mock_s3, mock_firehose -from tests.utils.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, REGION_NAME -from tests.utils.generic_setup_and_teardown_for_ack_backend import GenericSetUp, GenericTearDown +from tests.utils.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, + REGION_NAME, +) +from tests.utils.generic_setup_and_teardown_for_ack_backend import ( + GenericSetUp, + GenericTearDown, +) from tests.utils.utils_for_ack_backend_tests import ( setup_existing_ack_file, validate_ack_file_content, @@ -50,7 +57,7 @@ def setUp(self) -> None: Key=f"processing/{MOCK_MESSAGE_DETAILS.file_key}", Body=mock_source_file_with_100_rows.getvalue(), ) - self.logger_info_patcher = patch('common.log_decorator.logger.info') + self.logger_info_patcher = patch("common.log_decorator.logger.info") self.mock_logger_info = self.logger_info_patcher.start() def tearDown(self) -> None: @@ -77,7 +84,12 @@ def test_lambda_handler_main_multiple_records(self): """Test lambda handler with multiple records.""" # First array of messages: all successful. Rows 1 to 3 array_of_success_messages = [ - {**BASE_SUCCESS_MESSAGE, "row_id": f"row^{i}", "imms_id": f"imms_{i}", "local_id": f"local^{i}"} + { + **BASE_SUCCESS_MESSAGE, + "row_id": f"row^{i}", + "imms_id": f"imms_{i}", + "local_id": f"local^{i}", + } for i in range(1, 4) ] # Second array of messages: all with diagnostics (failure messages). Rows 4 to 7 @@ -92,8 +104,18 @@ def test_lambda_handler_main_multiple_records(self): "local_id": "local^8", "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR, }, - {**BASE_SUCCESS_MESSAGE, "row_id": "row^9", "imms_id": "imms_9", "local_id": "local^9"}, - {**BASE_SUCCESS_MESSAGE, "row_id": "row^10", "imms_id": "imms_10", "local_id": "local^10"}, + { + **BASE_SUCCESS_MESSAGE, + "row_id": "row^9", + "imms_id": "imms_9", + "local_id": "local^9", + }, + { + **BASE_SUCCESS_MESSAGE, + "row_id": "row^10", + "imms_id": "imms_10", + "local_id": "local^10", + }, { **BASE_FAILURE_MESSAGE, "row_id": "row^11", @@ -118,7 +140,7 @@ def test_lambda_handler_main_multiple_records(self): [ *array_of_success_messages, *array_of_failure_messages, - *array_of_mixed_success_and_failure_messages + *array_of_mixed_success_and_failure_messages, ], existing_file_content=ValidValues.ack_headers, ) @@ -133,22 +155,46 @@ def test_lambda_handler_main(self): { "description": "Multiple messages: all with diagnostics (failure messages)", "messages": [ - {"row_id": "row_1", "diagnostics": DiagnosticsDictionaries.UNIQUE_ID_MISSING}, - {"row_id": "row_2", "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS}, - {"row_id": "row_3", "diagnostics": DiagnosticsDictionaries.RESOURCE_NOT_FOUND_ERROR}, + { + "row_id": "row_1", + "diagnostics": DiagnosticsDictionaries.UNIQUE_ID_MISSING, + }, + { + "row_id": "row_2", + "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS, + }, + { + "row_id": "row_3", + "diagnostics": DiagnosticsDictionaries.RESOURCE_NOT_FOUND_ERROR, + }, ], }, { "description": "Multiple messages: mixture of success and failure messages", "messages": [ {"row_id": "row_1", "imms_id": "TEST_IMMS_ID"}, - {"row_id": "row_2", "diagnostics": DiagnosticsDictionaries.UNIQUE_ID_MISSING}, - {"row_id": "row_3", "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR}, + { + "row_id": "row_2", + "diagnostics": DiagnosticsDictionaries.UNIQUE_ID_MISSING, + }, + { + "row_id": "row_3", + "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR, + }, {"row_id": "row_4"}, - {"row_id": "row_5", "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR}, - {"row_id": "row_6", "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR}, + { + "row_id": "row_5", + "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR, + }, + { + "row_id": "row_6", + "diagnostics": DiagnosticsDictionaries.CUSTOM_VALIDATION_ERROR, + }, {"row_id": "row_7"}, - {"row_id": "row_8", "diagnostics": DiagnosticsDictionaries.IDENTIFIER_DUPLICATION_ERROR}, + { + "row_id": "row_8", + "diagnostics": DiagnosticsDictionaries.IDENTIFIER_DUPLICATION_ERROR, + }, ], }, { @@ -157,7 +203,12 @@ def test_lambda_handler_main(self): }, { "description": "Single row: malformed diagnostics info from forwarder", - "messages": [{"row_id": "row_1", "diagnostics": "SHOULD BE A DICTIONARY, NOT A STRING"}], + "messages": [ + { + "row_id": "row_1", + "diagnostics": "SHOULD BE A DICTIONARY, NOT A STRING", + } + ], }, ] @@ -168,7 +219,10 @@ def test_lambda_handler_main(self): self.assertEqual(response, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) validate_ack_file_content(self.s3_client, test_case["messages"]) - self.s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key) + self.s3_client.delete_object( + Bucket=BucketNames.DESTINATION, + Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key, + ) # Test scenario where there is an existing ack file # TODO: None of the test cases have any existing ack file content? @@ -176,13 +230,17 @@ def test_lambda_handler_main(self): existing_ack_file_content = test_case.get("existing_ack_file_content", "") setup_existing_ack_file( MOCK_MESSAGE_DETAILS.temp_ack_file_key, - existing_ack_file_content, self.s3_client + existing_ack_file_content, + self.s3_client, ) response = lambda_handler(event=self.generate_event(test_case["messages"]), context={}) self.assertEqual(response, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) validate_ack_file_content(self.s3_client, test_case["messages"], existing_ack_file_content) - self.s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key) + self.s3_client.delete_object( + Bucket=BucketNames.DESTINATION, + Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key, + ) def test_lambda_handler_error_scenarios(self): """Test that the lambda handler raises appropriate exceptions for malformed event data.""" @@ -203,7 +261,7 @@ def test_lambda_handler_error_scenarios(self): for test_case in test_cases: with self.subTest(msg=test_case["description"]): with patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose: - with self.assertRaises(Exception): + with self.assertRaises(ValueError): lambda_handler(event=test_case["event"], context={}) error_log = mock_send_log_to_firehose.call_args[0][1] self.assertIn(test_case["expected_message"], error_log["diagnostics"]) diff --git a/lambdas/ack_backend/tests/test_audit_table.py b/lambdas/ack_backend/tests/test_audit_table.py index 76c2a0a8b..cea36ea40 100644 --- a/lambdas/ack_backend/tests/test_audit_table.py +++ b/lambdas/ack_backend/tests/test_audit_table.py @@ -7,9 +7,9 @@ class TestAuditTable(unittest.TestCase): def setUp(self): - self.logger_patcher = patch('audit_table.logger') + self.logger_patcher = patch("audit_table.logger") self.mock_logger = self.logger_patcher.start() - self.dynamodb_client_patcher = patch('audit_table.dynamodb_client') + self.dynamodb_client_patcher = patch("audit_table.dynamodb_client") self.mock_dynamodb_client = self.dynamodb_client_patcher.start() def tearDown(self): diff --git a/lambdas/ack_backend/tests/test_convert_message_to_ack_row.py b/lambdas/ack_backend/tests/test_convert_message_to_ack_row.py index 983ab3ab9..45d56bed2 100644 --- a/lambdas/ack_backend/tests/test_convert_message_to_ack_row.py +++ b/lambdas/ack_backend/tests/test_convert_message_to_ack_row.py @@ -7,7 +7,10 @@ from tests.utils.mock_environment_variables import MOCK_ENVIRONMENT_DICT, REGION_NAME -from tests.utils.generic_setup_and_teardown_for_ack_backend import GenericSetUp, GenericTearDown +from tests.utils.generic_setup_and_teardown_for_ack_backend import ( + GenericSetUp, + GenericTearDown, +) from tests.utils.values_for_ack_backend_tests import ( DefaultValues, ValidValues, @@ -15,7 +18,10 @@ ) with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): - from convert_message_to_ack_row import convert_message_to_ack_row, get_error_message_for_ack_file + from convert_message_to_ack_row import ( + convert_message_to_ack_row, + get_error_message_for_ack_file, + ) s3_client = boto3_client("s3", region_name=REGION_NAME) firehose_client = boto3_client("firehose", region_name=REGION_NAME) @@ -44,13 +50,19 @@ def test_get_error_message_for_ack_file(self): self.assertEqual(None, get_error_message_for_ack_file(None)) # CASE: diagnostics is not a dictionary - self.assertEqual(diagnastics_unclear_error_message, get_error_message_for_ack_file("not a dict")) + self.assertEqual( + diagnastics_unclear_error_message, + get_error_message_for_ack_file("not a dict"), + ) # CASE: Server error self.assertEqual(server_error_message, get_error_message_for_ack_file({"statusCode": 500})) # CASE: Diagnostics dictionary missing error_message - self.assertEqual(diagnastics_unclear_error_message, get_error_message_for_ack_file({"statusCode": 400})) + self.assertEqual( + diagnastics_unclear_error_message, + get_error_message_for_ack_file({"statusCode": 400}), + ) # CASE: Correctly formatted diagnostics dictionary self.assertEqual( diff --git a/lambdas/ack_backend/tests/test_logging_decorators.py b/lambdas/ack_backend/tests/test_logging_decorators.py index 8a0c01041..f9b85d85f 100644 --- a/lambdas/ack_backend/tests/test_logging_decorators.py +++ b/lambdas/ack_backend/tests/test_logging_decorators.py @@ -6,9 +6,9 @@ class TestLoggingDecorators(unittest.TestCase): def setUp(self): # Patch logger and firehose_client - self.logger_patcher = patch('common.log_decorator.logger') + self.logger_patcher = patch("common.log_decorator.logger") self.mock_logger = self.logger_patcher.start() - self.firehose_patcher = patch('common.log_decorator.firehose_client') + self.firehose_patcher = patch("common.log_decorator.firehose_client") self.mock_firehose = self.firehose_patcher.start() def tearDown(self): @@ -52,7 +52,7 @@ def dummy_func(message, created_at_formatted_string): "vaccine_type": "type", "supplier": "sup", "local_id": "loc", - "operation_requested": "op" + "operation_requested": "op", } result = dummy_func(message, "2024-08-20T12:00:00Z") self.assertEqual(result, "ok") @@ -70,7 +70,7 @@ def dummy_func(message, created_at_formatted_string): "vaccine_type": "type", "supplier": "sup", "local_id": "loc", - "operation_requested": "op" + "operation_requested": "op", } with self.assertRaises(ValueError): dummy_func(message, "2024-08-20T12:00:00Z") diff --git a/lambdas/ack_backend/tests/test_splunk_logging.py b/lambdas/ack_backend/tests/test_splunk_logging.py index e5ddfb695..90229d89e 100644 --- a/lambdas/ack_backend/tests/test_splunk_logging.py +++ b/lambdas/ack_backend/tests/test_splunk_logging.py @@ -1,4 +1,5 @@ """Tests for ack lambda logging decorators""" + import unittest from unittest.mock import patch, call import json @@ -14,7 +15,10 @@ EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS, ) from tests.utils.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames -from tests.utils.generic_setup_and_teardown_for_ack_backend import GenericSetUp, GenericTearDown +from tests.utils.generic_setup_and_teardown_for_ack_backend import ( + GenericSetUp, + GenericTearDown, +) from tests.utils.utils_for_ack_backend_tests import generate_event with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): @@ -59,7 +63,10 @@ def run(self, result=None): patch("update_ack_file.logger"), # Time is incremented by 1.0 for each call to time.time for ease of testing. # Range is set to a large number (300) due to many calls being made to time.time for some tests. - patch("logging_decorators.time.time", side_effect=[0.0 + i for i in range(300)]), + patch( + "logging_decorators.time.time", + side_effect=[0.0 + i for i in range(300)], + ), ] # Set up the ExitStack. Note that patches need to be explicitly started so that they will be applied even when @@ -99,7 +106,11 @@ def expected_lambda_handler_logs(self, success: bool, number_of_rows, ingestion_ if success else ValidValues.lambda_handler_failure_expected_log ) - return {**base_log, "time_taken": time_taken, **({"diagnostics": diagnostics} if diagnostics else {})} + return { + **base_log, + "time_taken": time_taken, + **({"diagnostics": diagnostics} if diagnostics else {}), + } def test_splunk_logging_successful_rows(self): """Tests a single object in the body of the event""" @@ -109,9 +120,18 @@ def test_splunk_logging_successful_rows(self): patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose, # noqa: E999 patch("common.log_decorator.logger") as mock_logger, # noqa: E999 ): # noqa: E999 - result = lambda_handler(event=generate_event([{"operation_requested": operation}]), context={}) + result = lambda_handler( + event=generate_event([{"operation_requested": operation}]), + context={}, + ) - self.assertEqual(result, {"statusCode": 200, "body": json.dumps("Lambda function executed successfully!")}) + self.assertEqual( + result, + { + "statusCode": 200, + "body": json.dumps("Lambda function executed successfully!"), + }, + ) expected_first_logger_info_data = { **ValidValues.mock_message_expected_log_value, @@ -129,7 +149,7 @@ def test_splunk_logging_successful_rows(self): mock_send_log_to_firehose.assert_has_calls( [ call(self.stream_name, expected_first_logger_info_data), - call(self.stream_name, expected_second_logger_info_data) + call(self.stream_name, expected_second_logger_info_data), ] ) @@ -140,14 +160,16 @@ def test_splunk_logging_missing_data(self): patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose, # noqa: E999 patch("common.log_decorator.logger") as mock_logger, # noqa: E999 ): # noqa: E999 - with self.assertRaises(Exception): + with self.assertRaises(AttributeError): lambda_handler(event={"Records": [{"body": json.dumps([{"": "456"}])}]}, context={}) expected_first_logger_info_data = {**InvalidValues.logging_with_no_values} expected_first_logger_error_data = self.expected_lambda_handler_logs( - success=False, number_of_rows=1, ingestion_complete=False, - diagnostics="'NoneType' object has no attribute 'replace'" + success=False, + number_of_rows=1, + ingestion_complete=False, + diagnostics="'NoneType' object has no attribute 'replace'", ) first_logger_info_call_args = json.loads(self.extract_all_call_args_for_logger_info(mock_logger)[0]) @@ -156,9 +178,10 @@ def test_splunk_logging_missing_data(self): self.assertEqual(first_logger_error_call_args, expected_first_logger_error_data) self.assertEqual( - mock_send_log_to_firehose.call_args_list, [ + mock_send_log_to_firehose.call_args_list, + [ call(self.stream_name, expected_first_logger_info_data), - call(self.stream_name, expected_first_logger_error_data) + call(self.stream_name, expected_first_logger_error_data), ], ) @@ -169,12 +192,30 @@ def test_splunk_logging_statuscode_diagnostics( ): """'Tests the correct codes are returned for diagnostics""" test_cases = [ - {"diagnostics": DiagnosticsDictionaries.RESOURCE_FOUND_ERROR, "expected_code": 409}, - {"diagnostics": DiagnosticsDictionaries.RESOURCE_NOT_FOUND_ERROR, "expected_code": 404}, - {"diagnostics": DiagnosticsDictionaries.MESSAGE_NOT_SUCCESSFUL_ERROR, "expected_code": 500}, - {"diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS, "expected_code": 403}, - {"diagnostics": DiagnosticsDictionaries.IDENTIFIER_DUPLICATION_ERROR, "expected_code": 422}, - {"diagnostics": DiagnosticsDictionaries.UNHANDLED_ERROR, "expected_code": 500}, + { + "diagnostics": DiagnosticsDictionaries.RESOURCE_FOUND_ERROR, + "expected_code": 409, + }, + { + "diagnostics": DiagnosticsDictionaries.RESOURCE_NOT_FOUND_ERROR, + "expected_code": 404, + }, + { + "diagnostics": DiagnosticsDictionaries.MESSAGE_NOT_SUCCESSFUL_ERROR, + "expected_code": 500, + }, + { + "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS, + "expected_code": 403, + }, + { + "diagnostics": DiagnosticsDictionaries.IDENTIFIER_DUPLICATION_ERROR, + "expected_code": 422, + }, + { + "diagnostics": DiagnosticsDictionaries.UNHANDLED_ERROR, + "expected_code": 500, + }, ] for test_case in test_cases: @@ -182,7 +223,10 @@ def test_splunk_logging_statuscode_diagnostics( patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose, # noqa: E999 patch("common.log_decorator.logger") as mock_logger, # noqa: E999 ): # noqa: E999 - result = lambda_handler(event=generate_event([{"diagnostics": test_case["diagnostics"]}]), context={}) + result = lambda_handler( + event=generate_event([{"diagnostics": test_case["diagnostics"]}]), + context={}, + ) self.assertEqual(result, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) @@ -204,7 +248,7 @@ def test_splunk_logging_statuscode_diagnostics( mock_send_log_to_firehose.assert_has_calls( [ call(self.stream_name, expected_first_logger_info_data), - call(self.stream_name, expected_second_logger_info_data) + call(self.stream_name, expected_second_logger_info_data), ] ) @@ -220,9 +264,15 @@ def test_splunk_logging_multiple_rows(self): self.assertEqual(result, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) - expected_first_logger_info_data = {**ValidValues.mock_message_expected_log_value, "message_id": "test1"} + expected_first_logger_info_data = { + **ValidValues.mock_message_expected_log_value, + "message_id": "test1", + } - expected_second_logger_info_data = {**ValidValues.mock_message_expected_log_value, "message_id": "test2"} + expected_second_logger_info_data = { + **ValidValues.mock_message_expected_log_value, + "message_id": "test2", + } expected_third_logger_info_data = self.expected_lambda_handler_logs(success=True, number_of_rows=2) @@ -259,7 +309,11 @@ def test_splunk_logging_multiple_with_diagnostics( "operation_requested": "UPDATE", "diagnostics": DiagnosticsDictionaries.MESSAGE_NOT_SUCCESSFUL_ERROR, }, - {"row_id": "test3", "operation_requested": "DELETE", "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS}, + { + "row_id": "test3", + "operation_requested": "DELETE", + "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS, + }, ] with ( # noqa: E999 @@ -330,17 +384,18 @@ def test_splunk_update_ack_file_not_logged(self): with ( # noqa: E999 patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose, # noqa: E999 patch("common.log_decorator.logger") as mock_logger, # noqa: E999 - patch("update_ack_file.change_audit_table_status_to_processed") - as mock_change_audit_table_status_to_processed, # noqa: E999 + patch( + "update_ack_file.change_audit_table_status_to_processed" + ) as mock_change_audit_table_status_to_processed, # noqa: E999 ): # noqa: E999 result = lambda_handler(generate_event(messages), context={}) self.assertEqual(result, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) expected_secondlast_logger_info_data = { - **ValidValues.mock_message_expected_log_value, - "message_id": "test98", - } + **ValidValues.mock_message_expected_log_value, + "message_id": "test98", + } expected_last_logger_info_data = self.expected_lambda_handler_logs(success=True, number_of_rows=98) all_logger_info_call_args = self.extract_all_call_args_for_logger_info(mock_logger) @@ -369,22 +424,23 @@ def test_splunk_update_ack_file_logged(self): with ( # noqa: E999 patch("common.log_decorator.send_log_to_firehose") as mock_send_log_to_firehose, # noqa: E999 patch("common.log_decorator.logger") as mock_logger, # noqa: E999 - patch("update_ack_file.change_audit_table_status_to_processed") - as mock_change_audit_table_status_to_processed, # noqa: E999 + patch( + "update_ack_file.change_audit_table_status_to_processed" + ) as mock_change_audit_table_status_to_processed, # noqa: E999 ): # noqa: E999 result = lambda_handler(generate_event(messages), context={}) self.assertEqual(result, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS) expected_thirdlast_logger_info_data = { - **ValidValues.mock_message_expected_log_value, - "message_id": "test99", - } + **ValidValues.mock_message_expected_log_value, + "message_id": "test99", + } expected_secondlast_logger_info_data = { - **ValidValues.upload_ack_file_expected_log, - "message_id": "test1", - "time_taken": "1.0s" - } + **ValidValues.upload_ack_file_expected_log, + "message_id": "test1", + "time_taken": "1.0s", + } expected_last_logger_info_data = self.expected_lambda_handler_logs( success=True, number_of_rows=99, ingestion_complete=True ) diff --git a/lambdas/ack_backend/tests/test_update_ack_file.py b/lambdas/ack_backend/tests/test_update_ack_file.py index 12f8b27d4..c9ee7de7a 100644 --- a/lambdas/ack_backend/tests/test_update_ack_file.py +++ b/lambdas/ack_backend/tests/test_update_ack_file.py @@ -6,8 +6,15 @@ from moto import mock_s3 from tests.utils.values_for_ack_backend_tests import ValidValues, DefaultValues -from tests.utils.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, REGION_NAME -from tests.utils.generic_setup_and_teardown_for_ack_backend import GenericSetUp, GenericTearDown +from tests.utils.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, + REGION_NAME, +) +from tests.utils.generic_setup_and_teardown_for_ack_backend import ( + GenericSetUp, + GenericTearDown, +) from tests.utils.utils_for_ack_backend_tests import ( setup_existing_ack_file, obtain_current_ack_file_content, @@ -21,7 +28,11 @@ from io import StringIO with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): - from update_ack_file import obtain_current_ack_content, create_ack_data, update_ack_file + from update_ack_file import ( + obtain_current_ack_content, + create_ack_data, + update_ack_file, + ) firehose_client = boto3_client("firehose", region_name=REGION_NAME) @@ -30,6 +41,7 @@ @mock_s3 class TestUpdateAckFile(unittest.TestCase): """Tests for the functions in the update_ack_file module.""" + def setUp(self) -> None: self.s3_client = boto3_client("s3", region_name=REGION_NAME) GenericSetUp(self.s3_client) @@ -42,14 +54,16 @@ def setUp(self) -> None: Key=f"processing/{MOCK_MESSAGE_DETAILS.file_key}", Body=mock_source_file_with_100_rows.getvalue(), ) - self.logger_patcher = patch('update_ack_file.logger') + self.logger_patcher = patch("update_ack_file.logger") self.mock_logger = self.logger_patcher.start() def tearDown(self) -> None: GenericTearDown(self.s3_client) def validate_ack_file_content( - self, incoming_messages: list[dict], existing_file_content: str = ValidValues.ack_headers + self, + incoming_messages: list[dict], + existing_file_content: str = ValidValues.ack_headers, ) -> None: """ Obtains the ack file content and ensures that it matches the expected content (expected content is based @@ -79,7 +93,11 @@ def test_update_ack_file(self): ], "expected_rows": [ generate_expected_ack_file_row(success=True, imms_id=DefaultValues.imms_id), - generate_expected_ack_file_row(success=False, imms_id="TEST_IMMS_ID_1", diagnostics="DIAGNOSTICS"), + generate_expected_ack_file_row( + success=False, + imms_id="TEST_IMMS_ID_1", + diagnostics="DIAGNOSTICS", + ), generate_expected_ack_file_row(success=False, imms_id="", diagnostics="DIAGNOSTICS"), generate_expected_ack_file_row(success=False, imms_id="", diagnostics="DIAGNOSTICS"), generate_expected_ack_file_row(success=True, imms_id="TEST_IMMS_ID_2"), @@ -88,9 +106,18 @@ def test_update_ack_file(self): { "description": "Multiple rows With different diagnostics", "input_rows": [ - {**ValidValues.ack_data_failure_dict, "OPERATION_OUTCOME": "Error 1"}, - {**ValidValues.ack_data_failure_dict, "OPERATION_OUTCOME": "Error 2"}, - {**ValidValues.ack_data_failure_dict, "OPERATION_OUTCOME": "Error 3"}, + { + **ValidValues.ack_data_failure_dict, + "OPERATION_OUTCOME": "Error 1", + }, + { + **ValidValues.ack_data_failure_dict, + "OPERATION_OUTCOME": "Error 2", + }, + { + **ValidValues.ack_data_failure_dict, + "OPERATION_OUTCOME": "Error 3", + }, ], "expected_rows": [ generate_expected_ack_file_row(success=False, imms_id="", diagnostics="Error 1"), @@ -115,7 +142,10 @@ def test_update_ack_file(self): expected_ack_file_content = ValidValues.ack_headers + "\n".join(test_case["expected_rows"]) + "\n" self.assertEqual(expected_ack_file_content, actual_ack_file_content) - self.s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key) + self.s3_client.delete_object( + Bucket=BucketNames.DESTINATION, + Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key, + ) def test_update_ack_file_existing(self): """Test that update_ack_file correctly updates the ack file when there was an existing ack file""" @@ -123,7 +153,10 @@ def test_update_ack_file_existing(self): existing_content = generate_sample_existing_ack_content() setup_existing_ack_file(MOCK_MESSAGE_DETAILS.temp_ack_file_key, existing_content, self.s3_client) - ack_data_rows = [ValidValues.ack_data_success_dict, ValidValues.ack_data_failure_dict] + ack_data_rows = [ + ValidValues.ack_data_success_dict, + ValidValues.ack_data_failure_dict, + ] update_ack_file( file_key=MOCK_MESSAGE_DETAILS.file_key, message_id=MOCK_MESSAGE_DETAILS.message_id, @@ -179,8 +212,16 @@ def test_create_ack_data(self): } test_cases = [ - {"success": True, "imms_id": MOCK_MESSAGE_DETAILS.imms_id, "expected_result": success_expected_result}, - {"success": False, "diagnostics": "test diagnostics message", "expected_result": failure_expected_result}, + { + "success": True, + "imms_id": MOCK_MESSAGE_DETAILS.imms_id, + "expected_result": success_expected_result, + }, + { + "success": False, + "diagnostics": "test diagnostics message", + "expected_result": failure_expected_result, + }, ] for test_case in test_cases: diff --git a/lambdas/ack_backend/tests/test_update_ack_file_flow.py b/lambdas/ack_backend/tests/test_update_ack_file_flow.py index 9bb3cd75a..6ad16c148 100644 --- a/lambdas/ack_backend/tests/test_update_ack_file_flow.py +++ b/lambdas/ack_backend/tests/test_update_ack_file_flow.py @@ -14,33 +14,33 @@ def setUp(self): # Patch all AWS and external dependencies self.s3_client = boto3.client("s3", region_name="eu-west-2") - self.ack_bucket_name = 'my-ack-bucket' - self.source_bucket_name = 'my-source-bucket' - self.ack_bucket_patcher = patch('update_ack_file.get_ack_bucket_name', return_value=self.ack_bucket_name) + self.ack_bucket_name = "my-ack-bucket" + self.source_bucket_name = "my-source-bucket" + self.ack_bucket_patcher = patch("update_ack_file.get_ack_bucket_name", return_value=self.ack_bucket_name) self.mock_get_ack_bucket_name = self.ack_bucket_patcher.start() self.source_bucket_patcher = patch( - 'update_ack_file.get_source_bucket_name', - return_value=self.source_bucket_name + "update_ack_file.get_source_bucket_name", + return_value=self.source_bucket_name, ) self.mock_get_source_bucket_name = self.source_bucket_patcher.start() self.s3_client.create_bucket( Bucket=self.ack_bucket_name, - CreateBucketConfiguration={"LocationConstraint": "eu-west-2"} + CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}, ) self.s3_client.create_bucket( Bucket=self.source_bucket_name, - CreateBucketConfiguration={"LocationConstraint": "eu-west-2"} + CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}, ) - self.logger_patcher = patch('update_ack_file.logger') + self.logger_patcher = patch("update_ack_file.logger") self.mock_logger = self.logger_patcher.start() - self.get_row_count_patcher = patch('update_ack_file.get_row_count') + self.get_row_count_patcher = patch("update_ack_file.get_row_count") self.mock_get_row_count = self.get_row_count_patcher.start() - self.change_audit_status_patcher = patch('update_ack_file.change_audit_table_status_to_processed') + self.change_audit_status_patcher = patch("update_ack_file.change_audit_table_status_to_processed") self.mock_change_audit_status = self.change_audit_status_patcher.start() def tearDown(self): @@ -49,21 +49,21 @@ def tearDown(self): self.change_audit_status_patcher.stop() def test_audit_table_updated_correctly(self): - """ VED-167 - Test that the audit table has been updated correctly""" + """VED-167 - Test that the audit table has been updated correctly""" # Setup self.mock_get_row_count.side_effect = [3, 3] accumulated_csv_content = StringIO("header1|header2\n") ack_data_rows = [ {"a": 1, "b": 2, "row": "audit-test-1"}, {"a": 3, "b": 4, "row": "audit-test-2"}, - {"a": 5, "b": 6, "row": "audit-test-3"} + {"a": 5, "b": 6, "row": "audit-test-3"}, ] message_id = "msg-audit-table" file_key = "audit_table_test.csv" self.s3_client.put_object( Bucket=self.source_bucket_name, Key=f"processing/{file_key}", - Body="dummy content" + Body="dummy content", ) # Act update_ack_file.upload_ack_file( @@ -74,25 +74,21 @@ def test_audit_table_updated_correctly(self): accumulated_csv_content=accumulated_csv_content, ack_data_rows=ack_data_rows, archive_ack_file_key=f"forwardedFile/{file_key}", - file_key=file_key + file_key=file_key, ) # Assert: Only check audit table update self.mock_change_audit_status.assert_called_once_with(file_key, message_id) def test_move_file(self): - """ VED-167 test that the file has been moved to the appropriate location """ + """VED-167 test that the file has been moved to the appropriate location""" bucket_name = "move-bucket" file_key = "src/move_file_test.csv" dest_key = "dest/move_file_test.csv" self.s3_client.create_bucket( Bucket=bucket_name, - CreateBucketConfiguration={"LocationConstraint": "eu-west-2"} - ) - self.s3_client.put_object( - Bucket=bucket_name, - Key=file_key, - Body="dummy content" + CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}, ) + self.s3_client.put_object(Bucket=bucket_name, Key=file_key, Body="dummy content") update_ack_file.move_file(bucket_name, file_key, dest_key) # Assert the destination object exists response = self.s3_client.get_object(Bucket=bucket_name, Key=dest_key) diff --git a/lambdas/ack_backend/tests/utils/generic_setup_and_teardown_for_ack_backend.py b/lambdas/ack_backend/tests/utils/generic_setup_and_teardown_for_ack_backend.py index 013333c5a..f30dc8244 100644 --- a/lambdas/ack_backend/tests/utils/generic_setup_and_teardown_for_ack_backend.py +++ b/lambdas/ack_backend/tests/utils/generic_setup_and_teardown_for_ack_backend.py @@ -14,9 +14,14 @@ class GenericSetUp: def __init__(self, s3_client=None, firehose_client=None): if s3_client: - for bucket_name in [BucketNames.SOURCE, BucketNames.DESTINATION, BucketNames.MOCK_FIREHOSE]: + for bucket_name in [ + BucketNames.SOURCE, + BucketNames.DESTINATION, + BucketNames.MOCK_FIREHOSE, + ]: s3_client.create_bucket( - Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) if firehose_client: @@ -37,7 +42,11 @@ class GenericTearDown: def __init__(self, s3_client=None, firehose_client=None): if s3_client: - for bucket_name in [BucketNames.SOURCE, BucketNames.DESTINATION, BucketNames.MOCK_FIREHOSE]: + for bucket_name in [ + BucketNames.SOURCE, + BucketNames.DESTINATION, + BucketNames.MOCK_FIREHOSE, + ]: for obj in s3_client.list_objects_v2(Bucket=bucket_name).get("Contents", []): s3_client.delete_object(Bucket=bucket_name, Key=obj["Key"]) s3_client.delete_bucket(Bucket=bucket_name) diff --git a/lambdas/ack_backend/tests/utils/mock_environment_variables.py b/lambdas/ack_backend/tests/utils/mock_environment_variables.py index ae1fd599f..1a0d9641b 100644 --- a/lambdas/ack_backend/tests/utils/mock_environment_variables.py +++ b/lambdas/ack_backend/tests/utils/mock_environment_variables.py @@ -21,5 +21,5 @@ class Firehose: "ACK_BUCKET_NAME": BucketNames.DESTINATION, "FIREHOSE_STREAM_NAME": Firehose.STREAM_NAME, "AUDIT_TABLE_NAME": "immunisation-batch-internal-dev-audit-table", - "SOURCE_BUCKET_NAME": BucketNames.SOURCE + "SOURCE_BUCKET_NAME": BucketNames.SOURCE, } diff --git a/lambdas/ack_backend/tests/utils/utils_for_ack_backend_tests.py b/lambdas/ack_backend/tests/utils/utils_for_ack_backend_tests.py index 542fb8272..f2153743b 100644 --- a/lambdas/ack_backend/tests/utils/utils_for_ack_backend_tests.py +++ b/lambdas/ack_backend/tests/utils/utils_for_ack_backend_tests.py @@ -29,8 +29,7 @@ def setup_existing_ack_file(file_key, file_content, s3_client): s3_client.put_object(Bucket=BucketNames.DESTINATION, Key=file_key, Body=file_content) -def obtain_current_ack_file_content(s3_client, - temp_ack_file_key: str = MOCK_MESSAGE_DETAILS.temp_ack_file_key) -> str: +def obtain_current_ack_file_content(s3_client, temp_ack_file_key: str = MOCK_MESSAGE_DETAILS.temp_ack_file_key) -> str: """Obtains the ack file content from the destination bucket.""" retrieved_object = s3_client.get_object(Bucket=BucketNames.DESTINATION, Key=temp_ack_file_key) return retrieved_object["Body"].read().decode("utf-8") @@ -62,9 +61,7 @@ def generate_sample_existing_ack_content() -> str: return ValidValues.ack_headers + generate_expected_ack_file_row(success=True) -def generate_expected_ack_content( - incoming_messages: list[dict], existing_content: str = ValidValues.ack_headers -) -> str: +def generate_expected_ack_content(incoming_messages: list[dict], existing_content: str = ValidValues.ack_headers) -> str: """Returns the expected_ack_file_content based on the incoming messages""" for message in incoming_messages: # Determine diagnostics based on the diagnostics value in the incoming message @@ -80,10 +77,11 @@ def generate_expected_ack_content( success=diagnostics == "", row_id=message.get("row_id", MOCK_MESSAGE_DETAILS.row_id), created_at_formatted_string=message.get( - "created_at_formatted_string", MOCK_MESSAGE_DETAILS.created_at_formatted_string + "created_at_formatted_string", + MOCK_MESSAGE_DETAILS.created_at_formatted_string, ), local_id=message.get("local_id", MOCK_MESSAGE_DETAILS.local_id), - imms_id="" if diagnostics else message.get("imms_id", MOCK_MESSAGE_DETAILS.imms_id), + imms_id=("" if diagnostics else message.get("imms_id", MOCK_MESSAGE_DETAILS.imms_id)), diagnostics=diagnostics, ) @@ -95,7 +93,7 @@ def generate_expected_ack_content( def validate_ack_file_content( s3_client, incoming_messages: list[dict], - existing_file_content: str = ValidValues.ack_headers + existing_file_content: str = ValidValues.ack_headers, ) -> None: """ Obtains the ack file content and ensures that it matches the expected content (expected content is based diff --git a/lambdas/ack_backend/tests/utils/values_for_ack_backend_tests.py b/lambdas/ack_backend/tests/utils/values_for_ack_backend_tests.py index 346f57351..a3cf45ac8 100644 --- a/lambdas/ack_backend/tests/utils/values_for_ack_backend_tests.py +++ b/lambdas/ack_backend/tests/utils/values_for_ack_backend_tests.py @@ -134,7 +134,10 @@ def __init__( self.success_message = {**self.base_message, "imms_id": imms_id} - self.failure_message = {**self.base_message, "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS} + self.failure_message = { + **self.base_message, + "diagnostics": DiagnosticsDictionaries.NO_PERMISSIONS, + } class MockMessageDetails: diff --git a/lambdas/id_sync/README.md b/lambdas/id_sync/README.md index cfd2da256..cdb5da789 100644 --- a/lambdas/id_sync/README.md +++ b/lambdas/id_sync/README.md @@ -20,12 +20,12 @@ ## Configuration - **Environment Variables:** - - `IEDS_TABLE_NAME`: Name of events table. - - `PDS_ENV`: Targeted PDS service environment, eg INT. - - `SPLUNK_FIREHOSE_NAME`: Name of the splunk firehose for logging + - `IEDS_TABLE_NAME`: Name of events table. + - `PDS_ENV`: Targeted PDS service environment, eg INT. + - `SPLUNK_FIREHOSE_NAME`: Name of the splunk firehose for logging ## Development - Code is located in the `lambdas/id_sync/src/` directory. - Unit tests are in the `lambdas/id_sync/tests/` directory. -- Use the provided Makefile and Dockerfile for building, testing, and packaging. \ No newline at end of file +- Use the provided Makefile and Dockerfile for building, testing, and packaging. diff --git a/lambdas/id_sync/src/exceptions/id_sync_exception.py b/lambdas/id_sync/src/exceptions/id_sync_exception.py index 4ef9e49e7..06a11ec1c 100644 --- a/lambdas/id_sync/src/exceptions/id_sync_exception.py +++ b/lambdas/id_sync/src/exceptions/id_sync_exception.py @@ -1,5 +1,6 @@ class IdSyncException(Exception): """Custom exception for ID Sync errors.""" + def __init__(self, message: str, nhs_numbers: list = None, exception=None): self.message = message self.nhs_numbers = nhs_numbers diff --git a/lambdas/id_sync/src/id_sync.py b/lambdas/id_sync/src/id_sync.py index ef9b5f595..a79ca20a4 100644 --- a/lambdas/id_sync/src/id_sync.py +++ b/lambdas/id_sync/src/id_sync.py @@ -39,14 +39,16 @@ def handler(event_data: Dict[str, Any], _context) -> Dict[str, Any]: error_count += 1 if error_count > 0: - raise IdSyncException(message=f"Processed {len(records)} records with {error_count} errors", - nhs_numbers=nhs_numbers) + raise IdSyncException( + message=f"Processed {len(records)} records with {error_count} errors", + nhs_numbers=nhs_numbers, + ) response = { "status": "success", "message": f"Successfully processed {len(records)} records", - "nhs_numbers": nhs_numbers - } + "nhs_numbers": nhs_numbers, + } logger.info("id_sync handler completed: %s", response) return response diff --git a/lambdas/id_sync/src/ieds_db_operations.py b/lambdas/id_sync/src/ieds_db_operations.py index e918e6dae..2ecac4a7a 100644 --- a/lambdas/id_sync/src/ieds_db_operations.py +++ b/lambdas/id_sync/src/ieds_db_operations.py @@ -50,8 +50,7 @@ def ieds_update_patient_id(old_id: str, new_id: str, items_to_update: list | Non all_batches_successful, total_batches = execute_transaction_in_batches(transact_items) # Consolidated response handling - logger.info( - f"All batches complete. Total batches: {total_batches}, All successful: {all_batches_successful}") + logger.info(f"All batches complete. Total batches: {total_batches}, All successful: {all_batches_successful}") if all_batches_successful: return make_status( @@ -59,7 +58,11 @@ def ieds_update_patient_id(old_id: str, new_id: str, items_to_update: list | Non old_id, ) else: - return make_status(f"Failed to update some batches for patient ID: {old_id}", old_id, "error") + return make_status( + f"Failed to update some batches for patient ID: {old_id}", + old_id, + "error", + ) except Exception as e: logger.exception("Error updating patient ID") @@ -67,7 +70,7 @@ def ieds_update_patient_id(old_id: str, new_id: str, items_to_update: list | Non raise IdSyncException( message=f"Error updating patient Id from :{old_id} to {new_id}", nhs_numbers=[old_id, new_id], - exception=e + exception=e, ) @@ -102,7 +105,7 @@ def paginate_items_for_patient_pk(patient_pk: str) -> list: while True: query_args = { "IndexName": "PatientGSI", - "KeyConditionExpression": Key('PatientPK').eq(patient_pk), + "KeyConditionExpression": Key("PatientPK").eq(patient_pk), } if last_evaluated_key: query_args["ExclusiveStartKey"] = last_evaluated_key @@ -168,22 +171,24 @@ def build_transact_items(old_id: str, new_id: str, items_to_update: list) -> lis new_patient_pk = f"Patient#{new_id}" for item in items_to_update: - old_patient_pk = item.get('PatientPK', f"Patient#{old_id}") - - transact_items.append({ - 'Update': { - 'TableName': ieds_table_name, - 'Key': { - 'PK': {'S': item['PK']}, - }, - 'UpdateExpression': 'SET PatientPK = :new_val', - "ConditionExpression": "PatientPK = :expected_old", - 'ExpressionAttributeValues': { - ':new_val': {'S': new_patient_pk}, - ':expected_old': {'S': old_patient_pk} + old_patient_pk = item.get("PatientPK", f"Patient#{old_id}") + + transact_items.append( + { + "Update": { + "TableName": ieds_table_name, + "Key": { + "PK": {"S": item["PK"]}, + }, + "UpdateExpression": "SET PatientPK = :new_val", + "ConditionExpression": "PatientPK = :expected_old", + "ExpressionAttributeValues": { + ":new_val": {"S": new_patient_pk}, + ":expected_old": {"S": old_patient_pk}, + }, } } - }) + ) return transact_items @@ -197,7 +202,7 @@ def execute_transaction_in_batches(transact_items: list) -> tuple: total_batches = 0 for i in range(0, len(transact_items), BATCH_SIZE): - batch = transact_items[i:i+BATCH_SIZE] + batch = transact_items[i : i + BATCH_SIZE] total_batches += 1 logger.info(f"Transacting batch {total_batches} of size: {len(batch)}") @@ -205,9 +210,8 @@ def execute_transaction_in_batches(transact_items: list) -> tuple: logger.info("Batch update complete. Response: %s", response) # Check each batch response - if response['ResponseMetadata']['HTTPStatusCode'] != 200: + if response["ResponseMetadata"]["HTTPStatusCode"] != 200: all_batches_successful = False - logger.error( - f"Batch {total_batches} failed with status: {response['ResponseMetadata']['HTTPStatusCode']}") + logger.error(f"Batch {total_batches} failed with status: {response['ResponseMetadata']['HTTPStatusCode']}") return all_batches_successful, total_batches diff --git a/lambdas/id_sync/src/pds_details.py b/lambdas/id_sync/src/pds_details.py index e5be640c3..7f6271aa5 100644 --- a/lambdas/id_sync/src/pds_details.py +++ b/lambdas/id_sync/src/pds_details.py @@ -1,6 +1,7 @@ -''' - Operations related to PDS (Patient Demographic Service) -''' +""" +Operations related to PDS (Patient Demographic Service) +""" + import tempfile from os_vars import get_pds_env from common.authentication import AppRestrictedAuth, Service diff --git a/lambdas/id_sync/src/record_processor.py b/lambdas/id_sync/src/record_processor.py index 67263ea06..2918d9b18 100644 --- a/lambdas/id_sync/src/record_processor.py +++ b/lambdas/id_sync/src/record_processor.py @@ -14,7 +14,7 @@ def process_record(event_record: Dict[str, Any]) -> Dict[str, Any]: logger.info("process_record. Processing record: %s", event_record) - body_text = event_record.get('body', '') + body_text = event_record.get("body", "") # convert body to json (try JSON first, then fall back to Python literal) if isinstance(body_text, str): @@ -57,7 +57,10 @@ def process_nhs_number(nhs_number: str) -> Dict[str, Any]: logger.exception("process_nhs_number: failed to fetch demographic details: %s", e) return make_status(str(e), nhs_number, "error") - logger.info("Fetched IEDS resources. IEDS count: %d", len(ieds_resources) if ieds_resources else 0) + logger.info( + "Fetched IEDS resources. IEDS count: %d", + len(ieds_resources) if ieds_resources else 0, + ) if not ieds_resources: logger.info("No IEDS records returned for NHS number: %s", nhs_number) @@ -77,9 +80,7 @@ def process_nhs_number(nhs_number: str) -> Dict[str, Any]: logger.info("No records matched PDS demographics: %d", discarded_count) return make_status("No records matched PDS demographics; update skipped", nhs_number) - response = ieds_update_patient_id( - nhs_number, new_nhs_number, items_to_update=matching_records - ) + response = ieds_update_patient_id(nhs_number, new_nhs_number, items_to_update=matching_records) response["nhs_number"] = nhs_number # add counts for observability response["matched"] = len(matching_records) @@ -93,13 +94,19 @@ def fetch_pds_and_ieds_resources(nhs_number: str): try: pds = pds_get_patient_details(nhs_number) except Exception as e: - logger.exception("fetch_pds_and_ieds_resources: failed to fetch PDS details for %s", nhs_number) + logger.exception( + "fetch_pds_and_ieds_resources: failed to fetch PDS details for %s", + nhs_number, + ) raise RuntimeError("Failed to fetch PDS details") from e try: ieds = get_items_from_patient_id(nhs_number) except Exception as e: - logger.exception("fetch_pds_and_ieds_resources: failed to fetch IEDS items for %s", nhs_number) + logger.exception( + "fetch_pds_and_ieds_resources: failed to fetch IEDS items for %s", + nhs_number, + ) raise RuntimeError("Failed to fetch IEDS items") from e return pds, ieds @@ -133,6 +140,7 @@ def demographics_match(pds_details: dict, ieds_item: dict) -> bool: If required fields are missing or unparsable on the IEDS side the function returns False. """ try: + def normalize_strings(item: Any) -> str | None: return str(item).strip().lower() if item else None @@ -140,8 +148,12 @@ def normalize_strings(item: Any) -> str | None: pds_name = normalize_strings(extract_normalized_name_from_patient(pds_details)) pds_gender = normalize_strings(pds_details.get("gender")) pds_birth = normalize_strings(pds_details.get("birthDate")) - logger.debug("demographics_match: demographics match for name=%s, gender=%s, birthDate=%s", - pds_name, pds_gender, pds_birth) + logger.debug( + "demographics_match: demographics match for name=%s, gender=%s, birthDate=%s", + pds_name, + pds_gender, + pds_birth, + ) # Retrieve patient resource from IEDS item patient = extract_patient_resource_from_item(ieds_item) diff --git a/lambdas/id_sync/tests/test_id_sync.py b/lambdas/id_sync/tests/test_id_sync.py index d85b0293a..81ca598a1 100644 --- a/lambdas/id_sync/tests/test_id_sync.py +++ b/lambdas/id_sync/tests/test_id_sync.py @@ -2,7 +2,7 @@ from unittest.mock import patch, MagicMock -with patch('common.log_decorator.logging_decorator') as mock_decorator: +with patch("common.log_decorator.logging_decorator") as mock_decorator: mock_decorator.return_value = lambda f: f # Pass-through decorator from id_sync import handler from exceptions.id_sync_exception import IdSyncException @@ -13,36 +13,30 @@ class TestIdSyncHandler(unittest.TestCase): def setUp(self): """Set up all patches and test fixtures""" # Patch all dependencies - self.aws_lambda_event_patcher = patch('id_sync.AwsLambdaEvent') + self.aws_lambda_event_patcher = patch("id_sync.AwsLambdaEvent") self.mock_aws_lambda_event = self.aws_lambda_event_patcher.start() - self.process_record_patcher = patch('id_sync.process_record') + self.process_record_patcher = patch("id_sync.process_record") self.mock_process_record = self.process_record_patcher.start() - self.logger_patcher = patch('id_sync.logger') + self.logger_patcher = patch("id_sync.logger") self.mock_logger = self.logger_patcher.start() # Set up test data - self.single_sqs_event = { - 'Records': [ - { - 'body': '{"source":"aws:sqs","data":"test-data"}' - } - ] - } + self.single_sqs_event = {"Records": [{"body": '{"source":"aws:sqs","data":"test-data"}'}]} self.multi_sqs_event = { - 'Records': [ + "Records": [ { - 'body': ('{"source":"aws:sqs","data":"a"}'), + "body": ('{"source":"aws:sqs","data":"a"}'), }, { - 'body': ('{"source":"aws:sqs","data":"b"}'), - } + "body": ('{"source":"aws:sqs","data":"b"}'), + }, ] } - self.empty_event = {'Records': []} - self.no_records_event = {'someOtherKey': 'value'} + self.empty_event = {"Records": []} + self.no_records_event = {"someOtherKey": "value"} def tearDown(self): """Stop all patches""" @@ -57,7 +51,7 @@ def test_handler_success_single_record(self): self.mock_process_record.return_value = { "status": "success", - "nhs_number": "test-nhs-number" + "nhs_number": "test-nhs-number", } # Call handler @@ -80,7 +74,7 @@ def test_handler_success_multiple_records(self): self.mock_process_record.side_effect = [ {"status": "success", "nhs_number": "test-nhs-number-1"}, - {"status": "success", "nhs_number": "test-nhs-number-2"} + {"status": "success", "nhs_number": "test-nhs-number-2"}, ] # Call handler @@ -101,7 +95,7 @@ def test_handler_error_single_record(self): self.mock_process_record.return_value = { "status": "error", - "nhs_number": "failed-nhs-number" + "nhs_number": "failed-nhs-number", } # Call handler @@ -126,7 +120,7 @@ def test_handler_mixed_success_error(self): self.mock_process_record.side_effect = [ {"status": "success", "nhs_number": "test-nhs-number-1"}, {"status": "error", "nhs_number": "test-nhs-number-2"}, - {"status": "success", "nhs_number": "test-nhs-number-3"} + {"status": "success", "nhs_number": "test-nhs-number-3"}, ] # Call handler @@ -138,7 +132,10 @@ def test_handler_mixed_success_error(self): self.assertEqual(self.mock_process_record.call_count, 3) self.assertEqual(error.message, "Processed 3 records with 1 errors") - self.assertEqual(error.nhs_numbers, ["test-nhs-number-1", "test-nhs-number-2", "test-nhs-number-3"]) + self.assertEqual( + error.nhs_numbers, + ["test-nhs-number-1", "test-nhs-number-2", "test-nhs-number-3"], + ) def test_handler_all_records_fail(self): """Test handler when all records fail""" @@ -149,7 +146,7 @@ def test_handler_all_records_fail(self): self.mock_process_record.side_effect = [ {"status": "error", "nhs_number": "test-nhs-number-1"}, - {"status": "error", "nhs_number": "test-nhs-number-2"} + {"status": "error", "nhs_number": "test-nhs-number-2"}, ] # Call handler @@ -245,7 +242,7 @@ def test_handler_process_record_missing_nhs_number(self): # Return result without 'nhs_number' but with an 'error' status self.mock_process_record.return_value = { "status": "error", - "message": "Missing NHS number" + "message": "Missing NHS number", # No 'nhs_number' } @@ -269,7 +266,7 @@ def test_handler_context_parameter_ignored(self): self.mock_process_record.return_value = { "status": "success", - "nhs_number": "nnhs-number-01" + "nhs_number": "nnhs-number-01", } # Call handler with mock context @@ -295,7 +292,7 @@ def test_handler_error_count_tracking(self): {"status": "success", "nhs_number": good_num1}, {"status": "error", "nhs_number": bad_num1}, {"status": "error", "nhs_number": bad_num2}, - {"status": "success", "nhs_number": good_num2} + {"status": "success", "nhs_number": good_num2}, ] # Call handler @@ -305,9 +302,5 @@ def test_handler_error_count_tracking(self): # Assertions - should track 2 errors out of 4 records self.assertEqual(self.mock_process_record.call_count, 4) - self.assertEqual(exception.nhs_numbers, - [good_num1, - bad_num1, - bad_num2, - good_num2]) + self.assertEqual(exception.nhs_numbers, [good_num1, bad_num1, bad_num2, good_num2]) self.assertEqual(exception.message, "Processed 4 records with 2 errors") diff --git a/lambdas/id_sync/tests/test_ieds_db_operations.py b/lambdas/id_sync/tests/test_ieds_db_operations.py index b4a55060e..2138a7954 100644 --- a/lambdas/id_sync/tests/test_ieds_db_operations.py +++ b/lambdas/id_sync/tests/test_ieds_db_operations.py @@ -12,9 +12,7 @@ def test_extract_from_dict_with_contained_patient(self): item = { "Resource": { "resourceType": "Immunization", - "contained": [ - {"resourceType": "Patient", "id": "P1", "name": [{"family": "Doe"}]} - ], + "contained": [{"resourceType": "Patient", "id": "P1", "name": [{"family": "Doe"}]}], } } @@ -55,12 +53,12 @@ def setUp(self): ieds_db_operations.ieds_table = None # Mock get_ieds_table_name - self.get_ieds_table_name_patcher = patch('ieds_db_operations.get_ieds_table_name') + self.get_ieds_table_name_patcher = patch("ieds_db_operations.get_ieds_table_name") self.mock_get_ieds_table_name = self.get_ieds_table_name_patcher.start() - self.mock_get_ieds_table_name.return_value = 'test-ieds-table' + self.mock_get_ieds_table_name.return_value = "test-ieds-table" # mock logger.exception - self.logger_patcher = patch('ieds_db_operations.logger') + self.logger_patcher = patch("ieds_db_operations.logger") self.mock_logger = self.logger_patcher.start() def tearDown(self): @@ -75,7 +73,7 @@ def setUp(self): super().setUp() # Mock get_dynamodb_table function - self.get_dynamodb_table_patcher = patch('ieds_db_operations.get_dynamodb_table') + self.get_dynamodb_table_patcher = patch("ieds_db_operations.get_dynamodb_table") self.mock_get_dynamodb_table = self.get_dynamodb_table_patcher.start() # Create mock table object @@ -91,7 +89,7 @@ def tearDown(self): def test_get_ieds_table_first_call(self): """Test first call to get_ieds_table initializes the global variable""" # Arrange - table_name = 'test-ieds-table' + table_name = "test-ieds-table" self.mock_get_ieds_table_name.return_value = table_name # Act @@ -142,7 +140,7 @@ def test_get_ieds_table_exception_handling_get_table_name(self): def test_get_ieds_table_exception_handling_get_dynamodb_table(self): """Test exception handling when get_dynamodb_table fails""" # Arrange - table_name = 'test-ieds-table' + table_name = "test-ieds-table" self.mock_get_ieds_table_name.return_value = table_name self.mock_get_dynamodb_table.side_effect = Exception("Failed to get DynamoDB table") @@ -162,7 +160,7 @@ def test_get_ieds_table_exception_handling_get_dynamodb_table(self): def test_get_ieds_table_multiple_calls_same_session(self): """Test multiple calls in the same session use cached table""" # Arrange - table_name = 'test-ieds-table' + table_name = "test-ieds-table" self.mock_get_ieds_table_name.return_value = table_name # Act - Make multiple calls @@ -184,7 +182,7 @@ def test_get_ieds_table_multiple_calls_same_session(self): def test_get_ieds_table_reset_global_variable(self): """Test that resetting global variable forces re-initialization""" # Arrange - First call - table_name = 'test-ieds-table' + table_name = "test-ieds-table" self.mock_get_ieds_table_name.return_value = table_name # Act - First call @@ -207,7 +205,7 @@ def test_get_ieds_table_reset_global_variable(self): def test_get_ieds_table_with_different_table_names(self): """Test with different table names on different calls""" # Arrange - First call - table_name1 = 'test-ieds-table-1' + table_name1 = "test-ieds-table-1" self.mock_get_ieds_table_name.return_value = table_name1 # Act - First call @@ -215,7 +213,7 @@ def test_get_ieds_table_with_different_table_names(self): # Reset global variable and change table name ieds_db_operations.ieds_table = None - table_name2 = 'test-ieds-table-2' + table_name2 = "test-ieds-table-2" self.mock_get_ieds_table_name.return_value = table_name2 # Act - Second call with different table name @@ -229,7 +227,7 @@ def test_get_ieds_table_with_different_table_names(self): self.assertEqual(self.mock_get_ieds_table_name.call_count, 2) expected_calls = [ unittest.mock.call(table_name1), - unittest.mock.call(table_name2) + unittest.mock.call(table_name2), ] self.mock_get_dynamodb_table.assert_has_calls(expected_calls) @@ -268,7 +266,7 @@ def test_get_ieds_table_none_table_name(self): def test_get_ieds_table_global_variable_consistency(self): """Test that global variable is consistently updated""" # Arrange - table_name = 'test-ieds-table' + table_name = "test-ieds-table" self.mock_get_ieds_table_name.return_value = table_name # Verify initial state @@ -308,25 +306,25 @@ class TestUpdatePatientIdInIEDS(TestIedsDbOperations): def setUp(self): super().setUp() # Mock get_ieds_table() and subsequent calls - self.mock_get_ieds_table = patch('ieds_db_operations.get_ieds_table') + self.mock_get_ieds_table = patch("ieds_db_operations.get_ieds_table") self.mock_get_ieds_table_patcher = self.mock_get_ieds_table.start() self.mock_table = MagicMock() self.mock_get_ieds_table_patcher.return_value = self.mock_table - self.mock_dynamodb_client = patch('ieds_db_operations.dynamodb_client') + self.mock_dynamodb_client = patch("ieds_db_operations.dynamodb_client") self.mock_dynamodb_client_patcher = self.mock_dynamodb_client.start() # Mock transact_write_items (not update_item) self.mock_dynamodb_client_patcher.transact_write_items = MagicMock() # Mock get_items_from_patient_id - self.get_items_from_patient_id_patcher = patch('ieds_db_operations.get_items_from_patient_id') + self.get_items_from_patient_id_patcher = patch("ieds_db_operations.get_items_from_patient_id") self.mock_get_items_from_patient_id = self.get_items_from_patient_id_patcher.start() # Mock get_ieds_table_name - self.get_ieds_table_name_patcher = patch('ieds_db_operations.get_ieds_table_name') + self.get_ieds_table_name_patcher = patch("ieds_db_operations.get_ieds_table_name") self.mock_get_ieds_table_name_mock = self.get_ieds_table_name_patcher.start() - self.mock_get_ieds_table_name_mock.return_value = 'test-ieds-table' + self.mock_get_ieds_table_name_mock.return_value = "test-ieds-table" def test_ieds_update_patient_id_success(self): """Test successful patient ID update""" @@ -336,13 +334,16 @@ def test_ieds_update_patient_id_success(self): # Mock items to update mock_items = [ - {'PK': 'Patient#old-patient-123', 'PatientPK': 'Patient#old-patient-123'}, - {'PK': 'Patient#old-patient-123#record1', 'PatientPK': 'Patient#old-patient-123'} + {"PK": "Patient#old-patient-123", "PatientPK": "Patient#old-patient-123"}, + { + "PK": "Patient#old-patient-123#record1", + "PatientPK": "Patient#old-patient-123", + }, ] self.mock_get_items_from_patient_id.return_value = mock_items # Mock successful transact_write_items response - mock_transact_response = {'ResponseMetadata': {'HTTPStatusCode': 200}} + mock_transact_response = {"ResponseMetadata": {"HTTPStatusCode": 200}} self.mock_dynamodb_client_patcher.transact_write_items.return_value = mock_transact_response # Act @@ -351,7 +352,7 @@ def test_ieds_update_patient_id_success(self): # Assert - Update expected message to match actual implementation expected_result = { "status": "success", - "message": f"IEDS update, patient ID: {old_id}=>{new_id}. {len(mock_items)} updated 1." + "message": f"IEDS update, patient ID: {old_id}=>{new_id}. {len(mock_items)} updated 1.", } expected_result["nhs_number"] = old_id self.assertEqual(result, expected_result) @@ -369,11 +370,11 @@ def test_ieds_update_patient_id_non_200_response(self): new_id = "new-patient-456" # Mock items to update - mock_items = [{'PK': 'Patient#old-patient-123', 'PatientPK': 'Patient#old-patient-123'}] + mock_items = [{"PK": "Patient#old-patient-123", "PatientPK": "Patient#old-patient-123"}] self.mock_get_items_from_patient_id.return_value = mock_items # Mock failed transact_write_items response (not update_item) - mock_transact_response = {'ResponseMetadata': {'HTTPStatusCode': 400}} + mock_transact_response = {"ResponseMetadata": {"HTTPStatusCode": 400}} self.mock_dynamodb_client_patcher.transact_write_items.return_value = mock_transact_response # Act @@ -382,7 +383,7 @@ def test_ieds_update_patient_id_non_200_response(self): # Assert expected_result = { "status": "error", - "message": f"Failed to update some batches for patient ID: {old_id}" + "message": f"Failed to update some batches for patient ID: {old_id}", } expected_result["nhs_number"] = old_id self.assertEqual(result, expected_result) @@ -405,7 +406,7 @@ def test_ieds_update_patient_id_no_items_found(self): # Assert expected_result = { "status": "success", - "message": f"No items found to update for patient ID: {old_id}" + "message": f"No items found to update for patient ID: {old_id}", } expected_result["nhs_number"] = old_id self.assertEqual(result, expected_result) @@ -428,7 +429,7 @@ def test_ieds_update_patient_id_empty_old_id(self): # Assert expected_result = { "status": "error", - "message": "Old ID and New ID cannot be empty" + "message": "Old ID and New ID cannot be empty", } expected_result["nhs_number"] = old_id self.assertEqual(result, expected_result) @@ -449,7 +450,7 @@ def test_ieds_update_patient_id_empty_new_id(self): # Assert expected_result = { "status": "error", - "message": "Old ID and New ID cannot be empty" + "message": "Old ID and New ID cannot be empty", } expected_result["nhs_number"] = old_id self.assertEqual(result, expected_result) @@ -469,7 +470,7 @@ def test_ieds_update_patient_id_same_old_and_new_id(self): # Assert expected_result = { "status": "success", - "message": f"No change in patient ID: {patient_id}" + "message": f"No change in patient ID: {patient_id}", } expected_result["nhs_number"] = patient_id self.assertEqual(result, expected_result) @@ -485,7 +486,12 @@ def test_ieds_update_patient_id_update_exception(self): new_id = "new-patient-error" # Mock items to update - mock_items = [{'PK': 'Patient#old-patient-error', 'PatientPK': 'Patient#old-patient-error'}] + mock_items = [ + { + "PK": "Patient#old-patient-error", + "PatientPK": "Patient#old-patient-error", + } + ] self.mock_get_items_from_patient_id.return_value = mock_items test_exception = Exception("DynamoDB transact failed") @@ -514,10 +520,10 @@ def test_ieds_update_patient_id_special_characters(self): new_id = "new-patient&456*()+" # Mock items to update - mock_items = [{'PK': f'Patient#{old_id}', 'PatientPK': f'Patient#{old_id}'}] + mock_items = [{"PK": f"Patient#{old_id}", "PatientPK": f"Patient#{old_id}"}] self.mock_get_items_from_patient_id.return_value = mock_items - mock_transact_response = {'ResponseMetadata': {'HTTPStatusCode': 200}} + mock_transact_response = {"ResponseMetadata": {"HTTPStatusCode": 200}} self.mock_dynamodb_client_patcher.transact_write_items.return_value = mock_transact_response # Act @@ -525,7 +531,10 @@ def test_ieds_update_patient_id_special_characters(self): # Assert self.assertEqual(result["status"], "success") - self.assertEqual(result["message"], f"IEDS update, patient ID: {old_id}=>{new_id}. {len(mock_items)} updated 1.") + self.assertEqual( + result["message"], + f"IEDS update, patient ID: {old_id}=>{new_id}. {len(mock_items)} updated 1.", + ) # Verify transact_write_items was called with special characters self.mock_dynamodb_client_patcher.transact_write_items.assert_called_once() @@ -536,7 +545,7 @@ class TestGetItemsToUpdate(TestIedsDbOperations): def setUp(self): super().setUp() # Mock get_ieds_table() - self.mock_get_ieds_table = patch('ieds_db_operations.get_ieds_table') + self.mock_get_ieds_table = patch("ieds_db_operations.get_ieds_table") self.mock_get_ieds_table_patcher = self.mock_get_ieds_table.start() self.mock_table = MagicMock() self.mock_get_ieds_table_patcher.return_value = self.mock_table @@ -549,12 +558,15 @@ def test_get_items_from_patient_id_success(self): # Arrange patient_id = "test-patient-123" expected_items = [ - {'PK': f'Patient#{patient_id}', 'PatientPK': f'Patient#{patient_id}'}, - {'PK': f'Patient#{patient_id}#record1', 'PatientPK': f'Patient#{patient_id}'} + {"PK": f"Patient#{patient_id}", "PatientPK": f"Patient#{patient_id}"}, + { + "PK": f"Patient#{patient_id}#record1", + "PatientPK": f"Patient#{patient_id}", + }, ] self.mock_table.query.return_value = { - 'Items': expected_items, - 'Count': len(expected_items) + "Items": expected_items, + "Count": len(expected_items), } # Act @@ -570,10 +582,7 @@ def test_get_items_from_patient_id_no_records(self): """Test when no records are found for the patient ID""" # Arrange patient_id = "test-patient-no-records" - self.mock_table.query.return_value = { - 'Items': [], - 'Count': 0 - } + self.mock_table.query.return_value = {"Items": [], "Count": 0} # Act result = ieds_db_operations.get_items_from_patient_id(patient_id) @@ -585,56 +594,57 @@ def test_get_items_from_patient_id_no_records(self): class TestIedsDbOperationsConditional(unittest.TestCase): def setUp(self): # Patch logger to suppress output - self.logger_patcher = patch('ieds_db_operations.logger') + self.logger_patcher = patch("ieds_db_operations.logger") self.mock_logger = self.logger_patcher.start() # Patch get_ieds_table_name and get_ieds_table - self.get_ieds_table_name_patcher = patch('ieds_db_operations.get_ieds_table_name') + self.get_ieds_table_name_patcher = patch("ieds_db_operations.get_ieds_table_name") self.mock_get_ieds_table_name = self.get_ieds_table_name_patcher.start() - self.mock_get_ieds_table_name.return_value = 'test-table' + self.mock_get_ieds_table_name.return_value = "test-table" - self.get_ieds_table_patcher = patch('ieds_db_operations.get_ieds_table') + self.get_ieds_table_patcher = patch("ieds_db_operations.get_ieds_table") self.mock_get_ieds_table = self.get_ieds_table_patcher.start() # Patch dynamodb client - self.dynamodb_client_patcher = patch('ieds_db_operations.dynamodb_client') + self.dynamodb_client_patcher = patch("ieds_db_operations.dynamodb_client") self.mock_dynamodb_client = self.dynamodb_client_patcher.start() def tearDown(self): patch.stopall() def test_ieds_update_patient_id_empty_inputs(self): - res = ieds_db_operations.ieds_update_patient_id('', '') - self.assertEqual(res['status'], 'error') + res = ieds_db_operations.ieds_update_patient_id("", "") + self.assertEqual(res["status"], "error") def test_ieds_update_patient_id_same_ids(self): - res = ieds_db_operations.ieds_update_patient_id('a', 'a') - self.assertEqual(res['status'], 'success') + res = ieds_db_operations.ieds_update_patient_id("a", "a") + self.assertEqual(res["status"], "success") def test_ieds_update_with_items_to_update_uses_provided_list(self): - items = [{'PK': 'Patient#1'}, {'PK': 'Patient#1#r2'}] + items = [{"PK": "Patient#1"}, {"PK": "Patient#1#r2"}] # patch transact_write_items to return success self.mock_dynamodb_client.transact_write_items = MagicMock( - return_value={'ResponseMetadata': {'HTTPStatusCode': 200}}) + return_value={"ResponseMetadata": {"HTTPStatusCode": 200}} + ) - res = ieds_db_operations.ieds_update_patient_id('1', '2', items_to_update=items) - self.assertEqual(res['status'], 'success') + res = ieds_db_operations.ieds_update_patient_id("1", "2", items_to_update=items) + self.assertEqual(res["status"], "success") # ensure transact called at least once self.mock_dynamodb_client.transact_write_items.assert_called() def test_ieds_update_batches_multiple_calls(self): # create 60 items to force 3 batches (25,25,10) - items = [{'PK': f'Patient#old#{i}'} for i in range(60)] + items = [{"PK": f"Patient#old#{i}"} for i in range(60)] called = [] def fake_transact(TransactItems): called.append(len(TransactItems)) - return {'ResponseMetadata': {'HTTPStatusCode': 200}} + return {"ResponseMetadata": {"HTTPStatusCode": 200}} self.mock_dynamodb_client.transact_write_items = MagicMock(side_effect=fake_transact) - res = ieds_db_operations.ieds_update_patient_id('old', 'new', items_to_update=items) - self.assertEqual(res['status'], 'success') + res = ieds_db_operations.ieds_update_patient_id("old", "new", items_to_update=items) + self.assertEqual(res["status"], "success") # should have been called 3 times self.assertEqual(len(called), 3) self.assertEqual(called[0], 25) @@ -642,9 +652,10 @@ def fake_transact(TransactItems): self.assertEqual(called[2], 10) def test_ieds_update_non_200_response(self): - items = [{'PK': 'Patient#1'}] + items = [{"PK": "Patient#1"}] self.mock_dynamodb_client.transact_write_items = MagicMock( - return_value={'ResponseMetadata': {'HTTPStatusCode': 500}}) + return_value={"ResponseMetadata": {"HTTPStatusCode": 500}} + ) - res = ieds_db_operations.ieds_update_patient_id('1', '2', items_to_update=items) - self.assertEqual(res['status'], 'error') + res = ieds_db_operations.ieds_update_patient_id("1", "2", items_to_update=items) + self.assertEqual(res["status"], "error") diff --git a/lambdas/id_sync/tests/test_pds_details.py b/lambdas/id_sync/tests/test_pds_details.py index 7f0540d76..f490e9b26 100644 --- a/lambdas/id_sync/tests/test_pds_details.py +++ b/lambdas/id_sync/tests/test_pds_details.py @@ -11,27 +11,27 @@ def setUp(self): self.test_patient_id = "9912003888" # Patch all external dependencies - self.logger_patcher = patch('pds_details.logger') + self.logger_patcher = patch("pds_details.logger") self.mock_logger = self.logger_patcher.start() - self.secrets_manager_patcher = patch('pds_details.secrets_manager_client') + self.secrets_manager_patcher = patch("pds_details.secrets_manager_client") self.mock_secrets_manager = self.secrets_manager_patcher.start() - self.pds_env_patcher = patch('pds_details.get_pds_env') + self.pds_env_patcher = patch("pds_details.get_pds_env") self.mock_pds_env = self.pds_env_patcher.start() self.mock_pds_env.return_value = "test-env" - self.cache_patcher = patch('pds_details.Cache') + self.cache_patcher = patch("pds_details.Cache") self.mock_cache_class = self.cache_patcher.start() self.mock_cache_instance = MagicMock() self.mock_cache_class.return_value = self.mock_cache_instance - self.auth_patcher = patch('pds_details.AppRestrictedAuth') + self.auth_patcher = patch("pds_details.AppRestrictedAuth") self.mock_auth_class = self.auth_patcher.start() self.mock_auth_instance = MagicMock() self.mock_auth_class.return_value = self.mock_auth_instance - self.pds_service_patcher = patch('pds_details.PdsService') + self.pds_service_patcher = patch("pds_details.PdsService") self.mock_pds_service_class = self.pds_service_patcher.start() self.mock_pds_service_instance = MagicMock() self.mock_pds_service_class.return_value = self.mock_pds_service_instance @@ -44,12 +44,10 @@ def test_pds_get_patient_details_success(self): """Test successful retrieval of patient details""" # Arrange expected_patient_data = { - "identifier": [ - {"value": "9912003888"} - ], + "identifier": [{"value": "9912003888"}], "name": "John Doe", "birthDate": "1990-01-01", - "gender": "male" + "gender": "male", } self.mock_pds_service_instance.get_patient_details.return_value = expected_patient_data @@ -103,12 +101,16 @@ def test_pds_get_patient_details_pds_service_exception(self): # Assert self.assertEqual(exception.inner_exception, mock_exception) - self.assertEqual(exception.message, f"Error getting PDS patient details for {self.test_patient_id}") + self.assertEqual( + exception.message, + f"Error getting PDS patient details for {self.test_patient_id}", + ) self.assertEqual(exception.nhs_numbers, None) # Verify exception was logged self.mock_logger.exception.assert_called_once_with( - f"Error getting PDS patient details for {self.test_patient_id}") + f"Error getting PDS patient details for {self.test_patient_id}" + ) self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id) @@ -123,12 +125,16 @@ def test_pds_get_patient_details_cache_initialization_error(self): # Assert exception = context.exception - self.assertEqual(exception.message, f"Error getting PDS patient details for {self.test_patient_id}") + self.assertEqual( + exception.message, + f"Error getting PDS patient details for {self.test_patient_id}", + ) self.assertEqual(exception.nhs_numbers, None) # Verify exception was logged self.mock_logger.exception.assert_called_once_with( - f"Error getting PDS patient details for {self.test_patient_id}") + f"Error getting PDS patient details for {self.test_patient_id}" + ) self.mock_cache_class.assert_called_once() @@ -143,12 +149,16 @@ def test_pds_get_patient_details_auth_initialization_error(self): # Assert exception = context.exception - self.assertEqual(exception.message, f"Error getting PDS patient details for {self.test_patient_id}") + self.assertEqual( + exception.message, + f"Error getting PDS patient details for {self.test_patient_id}", + ) self.assertEqual(exception.nhs_numbers, None) # Verify exception was logged self.mock_logger.exception.assert_called_once_with( - f"Error getting PDS patient details for {self.test_patient_id}") + f"Error getting PDS patient details for {self.test_patient_id}" + ) def test_pds_get_patient_details_exception(self): """Test when logger.info throws an exception""" @@ -164,11 +174,13 @@ def test_pds_get_patient_details_exception(self): exception = context.exception # Assert self.assertEqual(exception.inner_exception, test_exception) - self.assertEqual(exception.message, f"Error getting PDS patient details for {test_nhs_number}") + self.assertEqual( + exception.message, + f"Error getting PDS patient details for {test_nhs_number}", + ) self.assertEqual(exception.nhs_numbers, None) # Verify logger.exception was called due to the caught exception - self.mock_logger.exception.assert_called_once_with( - f"Error getting PDS patient details for {test_nhs_number}") + self.mock_logger.exception.assert_called_once_with(f"Error getting PDS patient details for {test_nhs_number}") def test_pds_get_patient_details_different_patient_ids(self): """Test with different patient ID formats""" @@ -216,10 +228,10 @@ def setUp(self): self.test_nhs_number = "9912003888" # Patch all external dependencies - self.logger_patcher = patch('pds_details.logger') + self.logger_patcher = patch("pds_details.logger") self.mock_logger = self.logger_patcher.start() - self.pds_get_patient_details_patcher = patch('pds_details.pds_get_patient_details') + self.pds_get_patient_details_patcher = patch("pds_details.pds_get_patient_details") self.mock_pds_get_patient_details = self.pds_get_patient_details_patcher.start() def tearDown(self): @@ -243,7 +255,7 @@ def test_pds_get_patient_id_empty_identifier_array(self): # Arrange patient_data_empty_identifier = { "identifier": [], # Empty array - "name": "John Doe" + "name": "John Doe", } self.mock_pds_get_patient_details.return_value = patient_data_empty_identifier @@ -253,5 +265,8 @@ def test_pds_get_patient_id_empty_identifier_array(self): # Assert exception = context.exception - self.assertEqual(exception.message, f"Error getting PDS patient ID for {self.test_nhs_number}") + self.assertEqual( + exception.message, + f"Error getting PDS patient ID for {self.test_nhs_number}", + ) self.assertEqual(exception.nhs_numbers, None) diff --git a/lambdas/id_sync/tests/test_record_processor.py b/lambdas/id_sync/tests/test_record_processor.py index c0b0dba81..19e6e0715 100644 --- a/lambdas/id_sync/tests/test_record_processor.py +++ b/lambdas/id_sync/tests/test_record_processor.py @@ -8,20 +8,20 @@ class TestRecordProcessor(unittest.TestCase): def setUp(self): """Set up test fixtures and mocks""" # Patch logger - self.logger_patcher = patch('record_processor.logger') + self.logger_patcher = patch("record_processor.logger") self.mock_logger = self.logger_patcher.start() # PDS helpers - self.pds_get_patient_id_patcher = patch('record_processor.pds_get_patient_id') + self.pds_get_patient_id_patcher = patch("record_processor.pds_get_patient_id") self.mock_pds_get_patient_id = self.pds_get_patient_id_patcher.start() - self.pds_get_patient_details_patcher = patch('record_processor.pds_get_patient_details') + self.pds_get_patient_details_patcher = patch("record_processor.pds_get_patient_details") self.mock_pds_get_patient_details = self.pds_get_patient_details_patcher.start() - self.ieds_update_patient_id_patcher = patch('record_processor.ieds_update_patient_id') + self.ieds_update_patient_id_patcher = patch("record_processor.ieds_update_patient_id") self.mock_ieds_update_patient_id = self.ieds_update_patient_id_patcher.start() - self.get_items_from_patient_id_patcher = patch('record_processor.get_items_from_patient_id') + self.get_items_from_patient_id_patcher = patch("record_processor.get_items_from_patient_id") self.mock_get_items_from_patient_id = self.get_items_from_patient_id_patcher.start() def tearDown(self): @@ -75,7 +75,7 @@ def test_process_record_success_update_required(self): "gender": "male", "birthDate": "1980-01-01", } - ] + ], } } self.mock_get_items_from_patient_id.return_value = [matching_item] @@ -116,12 +116,12 @@ def test_process_record_demographics_mismatch_skips_update(self): "gender": "male", "birthDate": "1990-01-01", } - ] + ], } } self.mock_get_items_from_patient_id.return_value = [non_matching_item] - # Act + # Act result = process_record(test_sqs_record) # Assert @@ -162,7 +162,8 @@ def test_get_items_exception_aborts_update(self): self.mock_get_items_from_patient_id.return_value = [{"Resource": {}}] self.mock_pds_get_patient_details.return_value = { "name": [{"given": ["J"], "family": "K"}], - "gender": "male", "birthDate": "2000-01-01" + "gender": "male", + "birthDate": "2000-01-01", } self.mock_get_items_from_patient_id.side_effect = Exception("dynamo fail") @@ -177,24 +178,24 @@ def test_update_called_on_match(self): test_sqs_record = {"body": {"subject": nhs_number}} self.mock_pds_get_patient_id.return_value = pds_id self.mock_pds_get_patient_details.return_value = { - "name": [ - { - "given": ["Sarah"], - "family": "Fowley"} - ], + "name": [{"given": ["Sarah"], "family": "Fowley"}], "gender": "male", - "birthDate": "1956-07-09" + "birthDate": "1956-07-09", } item = { "Resource": { "resourceType": "Immunization", - "contained": [{ - "resourceType": "Patient", - "id": "PatM", - "name": [{"given": ["Sarah"], "family": "Fowley"}], - "gender": "male", "birthDate": "1956-07-09"} - ]} + "contained": [ + { + "resourceType": "Patient", + "id": "PatM", + "name": [{"given": ["Sarah"], "family": "Fowley"}], + "gender": "male", + "birthDate": "1956-07-09", + } + ], } + } self.mock_get_items_from_patient_id.return_value = [item] self.mock_ieds_update_patient_id.return_value = {"status": "success"} @@ -219,7 +220,7 @@ def test_process_record_no_records_exist(self): self.mock_pds_get_patient_id.assert_called_once() def test_process_record_pds_returns_none_id(self): - """Test when PDS returns none """ + """Test when PDS returns none""" # Arrange test_id = "12345a" self.mock_pds_get_patient_id.return_value = None @@ -257,21 +258,25 @@ def test_body_is_string(self): # Mock demographics so update proceeds self.mock_pds_get_patient_details.return_value = { "name": [{"given": ["A"], "family": "B"}], - "gender": "female", "birthDate": "1990-01-01" + "gender": "female", + "birthDate": "1990-01-01", } - self.mock_get_items_from_patient_id.return_value = [{ - "Resource": { - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Patient", - "id": "Pat3", - "name": [{"given": ["A"], "family": "B"}], - "gender": "female", "birthDate": "1990-01-01" - } - ] + self.mock_get_items_from_patient_id.return_value = [ + { + "Resource": { + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Patient", + "id": "Pat3", + "name": [{"given": ["A"], "family": "B"}], + "gender": "female", + "birthDate": "1990-01-01", + } + ], + } } - }] + ] self.mock_ieds_update_patient_id.return_value = {"status": "success"} # Act result = process_record(test_record) @@ -300,15 +305,11 @@ def test_process_record_birthdate_mismatch_skips_update(self): { "resourceType": "Patient", "id": "PatX", - "name": - [{ - "given": ["John"], - "family": "Doe" - }], + "name": [{"given": ["John"], "family": "Doe"}], "gender": "male", - "birthDate": "1980-01-02" + "birthDate": "1980-01-02", } - ] + ], } } self.mock_get_items_from_patient_id.return_value = [item] @@ -338,14 +339,11 @@ def test_process_record_gender_mismatch_skips_update(self): { "resourceType": "Patient", "id": "PatY", - "name": [{ - "given": ["Alex"], - "family": "Smith" - }], + "name": [{"given": ["Alex"], "family": "Smith"}], "gender": "male", - "birthDate": "1992-03-03" + "birthDate": "1992-03-03", } - ] + ], } } self.mock_get_items_from_patient_id.return_value = [item] @@ -371,14 +369,11 @@ def test_process_record_no_comparable_fields_skips_update(self): { "resourceType": "Patient", "id": "PatZ", - "name": [{ - "given": ["Zoe"], - "family": "Lee" - }], + "name": [{"given": ["Zoe"], "family": "Lee"}], "gender": "female", - "birthDate": "2000-01-01" + "birthDate": "2000-01-01", } - ] + ], } } self.mock_get_items_from_patient_id.return_value = [item] diff --git a/lambdas/mns_subscription/src/mns_service.py b/lambdas/mns_subscription/src/mns_service.py index f04ec8b45..cf8e4d7d5 100644 --- a/lambdas/mns_subscription/src/mns_service.py +++ b/lambdas/mns_subscription/src/mns_service.py @@ -11,14 +11,17 @@ ServerError, BadRequestError, TokenValidationError, - ConflictError + ConflictError, ) SQS_ARN = os.getenv("SQS_ARN") apigee_env = os.getenv("APIGEE_ENVIRONMENT", "int") -MNS_URL = "https://api.service.nhs.uk/multicast-notification-service/subscriptions" \ - if apigee_env == "prod" else "https://int.api.service.nhs.uk/multicast-notification-service/subscriptions" +MNS_URL = ( + "https://api.service.nhs.uk/multicast-notification-service/subscriptions" + if apigee_env == "prod" + else "https://int.api.service.nhs.uk/multicast-notification-service/subscriptions" +) class MnsService: @@ -26,9 +29,9 @@ def __init__(self, authenticator: AppRestrictedAuth): self.authenticator = authenticator self.access_token = self.authenticator.get_access_token() self.request_headers = { - 'Content-Type': 'application/fhir+json', - 'Authorization': f'Bearer {self.access_token}', - 'X-Correlation-ID': str(uuid.uuid4()) + "Content-Type": "application/fhir+json", + "Authorization": f"Bearer {self.access_token}", + "X-Correlation-ID": str(uuid.uuid4()), } self.subscription_payload = { "resourceType": "Subscription", @@ -38,17 +41,20 @@ def __init__(self, authenticator: AppRestrictedAuth): "channel": { "type": "message", "endpoint": SQS_ARN, - "payload": "application/json" - } - } + "payload": "application/json", + }, + } logging.info(f"Using SQS ARN for subscription: {SQS_ARN}") def subscribe_notification(self) -> dict | None: response = requests.post( - MNS_URL, headers=self.request_headers, - data=json.dumps(self.subscription_payload), timeout=15) + MNS_URL, + headers=self.request_headers, + data=json.dumps(self.subscription_payload), + timeout=15, + ) if response.status_code in (200, 201): return response.json() else: @@ -119,15 +125,22 @@ def check_delete_subscription(self): def raise_error_response(response): error_mapping = { 401: (TokenValidationError, "Token validation failed for the request"), - 400: (BadRequestError, "Bad request: Resource type or parameters incorrect"), - 403: (UnauthorizedError, "You don't have the right permissions for this request"), + 400: ( + BadRequestError, + "Bad request: Resource type or parameters incorrect", + ), + 403: ( + UnauthorizedError, + "You don't have the right permissions for this request", + ), 500: (ServerError, "Internal Server Error"), 404: (ResourceNotFoundError, "Subscription or Resource not found"), - 409: (ConflictError, "SQS Queue Already Subscribed, can't re-subscribe") + 409: (ConflictError, "SQS Queue Already Subscribed, can't re-subscribe"), } exception_class, error_message = error_mapping.get( response.status_code, - (UnhandledResponseError, f"Unhandled error: {response.status_code}")) + (UnhandledResponseError, f"Unhandled error: {response.status_code}"), + ) if response.status_code == 404: raise exception_class(resource_type=response.json(), resource_id=error_message) diff --git a/lambdas/mns_subscription/tests/test_mns_service.py b/lambdas/mns_subscription/tests/test_mns_service.py index 975d457a5..f90d5375d 100644 --- a/lambdas/mns_subscription/tests/test_mns_service.py +++ b/lambdas/mns_subscription/tests/test_mns_service.py @@ -9,7 +9,8 @@ TokenValidationError, BadRequestError, UnauthorizedError, - ResourceNotFoundError) + ResourceNotFoundError, +) SQS_ARN = "arn:aws:sqs:eu-west-2:123456789012:my-queue" @@ -85,14 +86,7 @@ def test_get_subscription_success(self, mock_get): # Arrange a bundle with a matching entry mock_response = MagicMock() mock_response.status_code = 200 - mock_response.json.return_value = {"entry": [ - { - "channel": { - "endpoint": SQS_ARN - }, - "id": "123" - }] - } + mock_response.json.return_value = {"entry": [{"channel": {"endpoint": SQS_ARN}, "id": "123"}]} mock_get.return_value = mock_response service = MnsService(self.authenticator) @@ -155,11 +149,7 @@ def test_delete_subscription_success(self, mock_delete): service = MnsService(self.authenticator) result = service.delete_subscription("sub-id-123") self.assertTrue(result) - mock_delete.assert_called_with( - f"{MNS_URL}/sub-id-123", - headers=service.request_headers, - timeout=10 - ) + mock_delete.assert_called_with(f"{MNS_URL}/sub-id-123", headers=service.request_headers, timeout=10) @patch("mns_service.requests.delete") def test_delete_subscription_401(self, mock_delete): @@ -274,7 +264,10 @@ def test_400_bad_request_error(self): with self.assertRaises(BadRequestError) as context: MnsService.raise_error_response(resp) self.assertIn("Bad request: Resource type or parameters incorrect", str(context.exception)) - self.assertEqual(context.exception.message, "Bad request: Resource type or parameters incorrect") + self.assertEqual( + context.exception.message, + "Bad request: Resource type or parameters incorrect", + ) self.assertEqual(context.exception.response, {"resource": "Invalid"}) def test_unhandled_status_code(self): diff --git a/lambdas/mns_subscription/tests/test_mns_setup.py b/lambdas/mns_subscription/tests/test_mns_setup.py index 112bba868..9616d5232 100644 --- a/lambdas/mns_subscription/tests/test_mns_setup.py +++ b/lambdas/mns_subscription/tests/test_mns_setup.py @@ -24,7 +24,7 @@ def test_get_mns_service(self, mock_mns_service, mock_app_auth, mock_boto_client # Assert self.assertEqual(result, mock_mns_instance) - mock_boto_client.assert_called_once_with("secretsmanager", config=mock_boto_client.call_args[1]['config']) + mock_boto_client.assert_called_once_with("secretsmanager", config=mock_boto_client.call_args[1]["config"]) mock_app_auth.assert_called_once() mock_mns_service.assert_called_once_with(mock_auth_instance) diff --git a/lambdas/redis_sync/README.md b/lambdas/redis_sync/README.md index c5027e5dd..4c693273e 100644 --- a/lambdas/redis_sync/README.md +++ b/lambdas/redis_sync/README.md @@ -22,10 +22,10 @@ ## Configuration - **Environment Variables:** - - `CONFIG_BUCKET_NAME`: Name of the S3 bucket to monitor. - - `AWS_REGION`: AWS region for S3 and Redis. - - `REDIS_HOST`: Redis endpoint. - - `REDIS_PORT`: Redis port (default: 6379). + - `CONFIG_BUCKET_NAME`: Name of the S3 bucket to monitor. + - `AWS_REGION`: AWS region for S3 and Redis. + - `REDIS_HOST`: Redis endpoint. + - `REDIS_PORT`: Redis port (default: 6379). ## Usage @@ -41,4 +41,4 @@ ## License -This project is maintained by NHS. See [LICENSE](../LICENSE) for details. \ No newline at end of file +This project is maintained by NHS. See [LICENSE](../LICENSE) for details. diff --git a/lambdas/redis_sync/src/record_processor.py b/lambdas/redis_sync/src/record_processor.py index 40232714b..e5d732eb2 100644 --- a/lambdas/redis_sync/src/record_processor.py +++ b/lambdas/redis_sync/src/record_processor.py @@ -1,23 +1,25 @@ from redis_cacher import RedisCacher from common.clients import logger from common.s3_event import S3EventRecord -''' + +""" Record Processor This module processes individual S3 records from an event. It is used to upload data to Redis ElastiCache. -''' +""" def process_record(record: S3EventRecord) -> dict: try: - logger.info("Processing S3 r bucket: %s, key: %s", - record.get_bucket_name(), record.get_object_key()) + logger.info( + "Processing S3 r bucket: %s, key: %s", + record.get_bucket_name(), + record.get_object_key(), + ) bucket_name = record.get_bucket_name() file_key = record.get_object_key() - base_log_data = { - "file_key": file_key - } + base_log_data = {"file_key": file_key} try: result = RedisCacher.upload(bucket_name, file_key) diff --git a/lambdas/redis_sync/src/redis_cacher.py b/lambdas/redis_sync/src/redis_cacher.py index 93b35350c..aba419e79 100644 --- a/lambdas/redis_sync/src/redis_cacher.py +++ b/lambdas/redis_sync/src/redis_cacher.py @@ -13,7 +13,11 @@ class RedisCacher: @staticmethod def upload(bucket_name: str, file_key: str) -> dict: try: - logger.info("Upload from s3 to Redis cache. file '%s'. bucket '%s'", file_key, bucket_name) + logger.info( + "Upload from s3 to Redis cache. file '%s'. bucket '%s'", + file_key, + bucket_name, + ) # get from s3 config_file_content = S3Reader.read(bucket_name, file_key) @@ -31,10 +35,7 @@ def upload(bucket_name: str, file_key: str) -> dict: redis_client = get_redis_client() for key, mapping in redis_mappings.items(): - safe_mapping = { - k: json.dumps(v) if isinstance(v, list) else v - for k, v in mapping.items() - } + safe_mapping = {k: json.dumps(v) if isinstance(v, list) else v for k, v in mapping.items()} existing_mapping = redis_client.hgetall(key) logger.info("Existing mapping for %s: %s", key, existing_mapping) redis_client.hmset(key, safe_mapping) @@ -44,7 +45,10 @@ def upload(bucket_name: str, file_key: str) -> dict: redis_client.hdel(key, *fields_to_delete) logger.info("Deleted mapping fields for %s: %s", key, fields_to_delete) - return {"status": "success", "message": f"File {file_key} uploaded to Redis cache."} + return { + "status": "success", + "message": f"File {file_key} uploaded to Redis cache.", + } except Exception: msg = f"Error uploading file '{file_key}' to Redis cache" logger.exception(msg) diff --git a/lambdas/redis_sync/src/redis_sync.py b/lambdas/redis_sync/src/redis_sync.py index 8b3362668..fc2a11da1 100644 --- a/lambdas/redis_sync/src/redis_sync.py +++ b/lambdas/redis_sync/src/redis_sync.py @@ -4,10 +4,11 @@ from common.log_decorator import logging_decorator from common.redis_client import get_redis_client from common.s3_event import S3Event -''' + +""" Event Processor The Business Logic for the Redis Sync Lambda Function. - This module processes S3 events and iterates through each record to process them individually.''' + This module processes S3 events and iterates through each record to process them individually.""" def _process_all_records(s3_records: list) -> dict: @@ -21,12 +22,18 @@ def _process_all_records(s3_records: list) -> dict: error_count += 1 if error_count > 0: logger.error("Processed %d records with %d errors", record_count, error_count) - return {"status": "error", "message": f"Processed {record_count} records with {error_count} errors", - "file_keys": file_keys} + return { + "status": "error", + "message": f"Processed {record_count} records with {error_count} errors", + "file_keys": file_keys, + } else: logger.info("Successfully processed all %d records", record_count) - return {"status": "success", "message": f"Successfully processed {record_count} records", - "file_keys": file_keys} + return { + "status": "success", + "message": f"Successfully processed {record_count} records", + "file_keys": file_keys, + } @logging_decorator(prefix="redis_sync", stream_name=STREAM_NAME) @@ -38,7 +45,7 @@ def handler(event, _): if "read" in event: return read_event(get_redis_client(), event, logger) elif "Records" in event: - logger.info("Processing S3 event with %d records", len(event.get('Records', []))) + logger.info("Processing S3 event with %d records", len(event.get("Records", []))) s3_records = S3Event(event).get_s3_records() if not s3_records: logger.info(no_records) diff --git a/lambdas/redis_sync/src/transform_configs.py b/lambdas/redis_sync/src/transform_configs.py index 783464839..24baa6776 100644 --- a/lambdas/redis_sync/src/transform_configs.py +++ b/lambdas/redis_sync/src/transform_configs.py @@ -7,20 +7,15 @@ def transform_vaccine_map(mapping): logger.info("source data: %s", mapping) vacc_to_diseases = { - entry["vacc_type"]: entry["diseases"] - for entry in mapping - if "vacc_type" in entry and "diseases" in entry + entry["vacc_type"]: entry["diseases"] for entry in mapping if "vacc_type" in entry and "diseases" in entry } diseases_to_vacc = { - ':'.join(sorted(disease['code'] for disease in entry['diseases'])): entry['vacc_type'] + ":".join(sorted(disease["code"] for disease in entry["diseases"])): entry["vacc_type"] for entry in mapping if "diseases" in entry and "vacc_type" in entry } - return { - "vacc_to_diseases": vacc_to_diseases, - "diseases_to_vacc": diseases_to_vacc - } + return {"vacc_to_diseases": vacc_to_diseases, "diseases_to_vacc": diseases_to_vacc} def transform_supplier_permissions(mapping): @@ -31,9 +26,7 @@ def transform_supplier_permissions(mapping): logger.info("source data: %s", mapping) supplier_permissions = { - entry["supplier"]: entry["permissions"] - for entry in mapping - if "supplier" in entry and "permissions" in entry + entry["supplier"]: entry["permissions"] for entry in mapping if "supplier" in entry and "permissions" in entry } ods_code_to_supplier = { ods_code: entry["supplier"] @@ -44,7 +37,7 @@ def transform_supplier_permissions(mapping): return { "supplier_permissions": supplier_permissions, - "ods_code_to_supplier": ods_code_to_supplier + "ods_code_to_supplier": ods_code_to_supplier, } diff --git a/lambdas/redis_sync/src/transform_map.py b/lambdas/redis_sync/src/transform_map.py index dd31c11cf..578ae0cf1 100644 --- a/lambdas/redis_sync/src/transform_map.py +++ b/lambdas/redis_sync/src/transform_map.py @@ -1,11 +1,14 @@ from constants import RedisCacheKey from transform_configs import ( - transform_vaccine_map, transform_supplier_permissions, transform_validation_rules + transform_vaccine_map, + transform_supplier_permissions, + transform_validation_rules, ) from common.clients import logger -''' + +""" Transform config file to format required in REDIS cache. -''' +""" def transform_map(data, file_type) -> dict: diff --git a/lambdas/redis_sync/tests/test_data/permissions_config.json b/lambdas/redis_sync/tests/test_data/permissions_config.json index 46fa3dc87..e4b9fe563 100644 --- a/lambdas/redis_sync/tests/test_data/permissions_config.json +++ b/lambdas/redis_sync/tests/test_data/permissions_config.json @@ -1,87 +1,87 @@ -[ - { - "supplier": "DPSFULL", - "permissions": ["COVID19.CRUDS", "FLU.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], - "ods_codes": ["DPSFULL"] - }, - { - "supplier": "DPSREDUCED", - "permissions": ["COVID19.CRUDS", "FLU.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], - "ods_codes": ["DPSREDUCED"] - }, - { - "supplier": "EMIS", - "permissions": ["RSV.U"], - "ods_codes": ["YGM41", "YGJ"] - }, - { - "supplier": "PINNACLE", - "ods_codes": ["8J1100001"] - }, - { - "supplier": "SONAR", - "permissions": ["FLU.CD"], - "ods_codes": ["8HK48"] - }, - { - "supplier": "TPP", - "ods_codes": ["YGA"] - }, - { - "supplier": "AGEM-NIVS", - "ods_codes": ["0DE"] - }, - { - "supplier": "NIMS", - "ods_codes": ["0DF"] - }, - { - "supplier": "EVA", - "permissions": ["COVID19.CUD"], - "ods_codes": ["8HA94"] - }, - { - "supplier": "RAVS", - "ods_codes": ["X26"] - }, - { - "supplier": "MEDICAL_DIRECTOR", - "ods_codes": ["YGMYH"] - }, - { - "supplier": "COVID19_VACCINE_RESOLUTION_SERVICEDESK", - "ods_codes": ["N2N9I"] - }, - { - "supplier": "MAVIS", - "ods_codes": ["V0V8L"] - }, - { - "supplier": "MEDICUS", - "ods_codes": ["YGMYW"] - }, - { - "supplier": "POSITIVE-SOLUTIONS", - "ods_codes": ["YGM17"] - }, - { - "supplier": "CEGEDIM", - "ods_codes": ["YGM04"] - }, - { - "supplier": "WELSH_DA_1", - "ods_codes": ["W00"] - }, - { - "supplier": "WELSH_DA_2", - "ods_codes": ["W000"] - }, - { - "supplier": "NORTHERN_IRELAND_DA", - "ods_codes": ["ZT001"] - }, - { - "supplier": "SCOTLAND_DA", - "ods_codes": ["YA7"] - } -] +[ + { + "supplier": "DPSFULL", + "permissions": ["COVID19.CRUDS", "FLU.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], + "ods_codes": ["DPSFULL"] + }, + { + "supplier": "DPSREDUCED", + "permissions": ["COVID19.CRUDS", "FLU.CRUDS", "MMR.CRUDS", "RSV.CRUDS"], + "ods_codes": ["DPSREDUCED"] + }, + { + "supplier": "EMIS", + "permissions": ["RSV.U"], + "ods_codes": ["YGM41", "YGJ"] + }, + { + "supplier": "PINNACLE", + "ods_codes": ["8J1100001"] + }, + { + "supplier": "SONAR", + "permissions": ["FLU.CD"], + "ods_codes": ["8HK48"] + }, + { + "supplier": "TPP", + "ods_codes": ["YGA"] + }, + { + "supplier": "AGEM-NIVS", + "ods_codes": ["0DE"] + }, + { + "supplier": "NIMS", + "ods_codes": ["0DF"] + }, + { + "supplier": "EVA", + "permissions": ["COVID19.CUD"], + "ods_codes": ["8HA94"] + }, + { + "supplier": "RAVS", + "ods_codes": ["X26"] + }, + { + "supplier": "MEDICAL_DIRECTOR", + "ods_codes": ["YGMYH"] + }, + { + "supplier": "COVID19_VACCINE_RESOLUTION_SERVICEDESK", + "ods_codes": ["N2N9I"] + }, + { + "supplier": "MAVIS", + "ods_codes": ["V0V8L"] + }, + { + "supplier": "MEDICUS", + "ods_codes": ["YGMYW"] + }, + { + "supplier": "POSITIVE-SOLUTIONS", + "ods_codes": ["YGM17"] + }, + { + "supplier": "CEGEDIM", + "ods_codes": ["YGM04"] + }, + { + "supplier": "WELSH_DA_1", + "ods_codes": ["W00"] + }, + { + "supplier": "WELSH_DA_2", + "ods_codes": ["W000"] + }, + { + "supplier": "NORTHERN_IRELAND_DA", + "ods_codes": ["ZT001"] + }, + { + "supplier": "SCOTLAND_DA", + "ods_codes": ["YA7"] + } +] diff --git a/lambdas/redis_sync/tests/test_data/s3-notification-single-filename.json b/lambdas/redis_sync/tests/test_data/s3-notification-single-filename.json index 0025ac3b8..071520de0 100644 --- a/lambdas/redis_sync/tests/test_data/s3-notification-single-filename.json +++ b/lambdas/redis_sync/tests/test_data/s3-notification-single-filename.json @@ -1,36 +1,36 @@ { - "Records": [ - { - "eventVersion": "2.1", - "eventSource": "aws:s3", - "awsRegion": "eu-west-2", - "eventTime": "2025-06-11T17:23:46.990Z", - "eventName": "ObjectCreated:Put", - "userIdentity": { - "principalId": "AWS:AROAVA5YK2MEBRIFRJLOJ:STWA21@hscic.gov.uk" - }, - "requestParameters": { - "sourceIPAddress": "11.222.333.44" - }, - "responseElements": { - "x-amz-request-id": "amz-request-id-value", - "x-amz-id-2": "amz-id-2-value" - }, - "s3": { - "s3SchemaVersion": "1.0", - "configurationId": "tf-s3-lambda-20250610075911685100000001", - "bucket": { - "name": "imms-internal-dev-supplier-config", - "arn": "arn:aws:s3:::imms-internal-dev-supplier-config" - }, - "object": { - "key": "disease_vaccine.json", - "size": 1345, - "eTag": "etag-value", - "versionId": "versionId-value", - "sequencer": "sequencer-value" - } - } + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "eu-west-2", + "eventTime": "2025-06-11T17:23:46.990Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "AWS:AROAVA5YK2MEBRIFRJLOJ:STWA21@hscic.gov.uk" + }, + "requestParameters": { + "sourceIPAddress": "11.222.333.44" + }, + "responseElements": { + "x-amz-request-id": "amz-request-id-value", + "x-amz-id-2": "amz-id-2-value" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "tf-s3-lambda-20250610075911685100000001", + "bucket": { + "name": "imms-internal-dev-supplier-config", + "arn": "arn:aws:s3:::imms-internal-dev-supplier-config" + }, + "object": { + "key": "disease_vaccine.json", + "size": 1345, + "eTag": "etag-value", + "versionId": "versionId-value", + "sequencer": "sequencer-value" } - ] -} \ No newline at end of file + } + } + ] +} diff --git a/lambdas/redis_sync/tests/test_data/test_read_vaccine_mapping.json b/lambdas/redis_sync/tests/test_data/test_read_vaccine_mapping.json index e92aa2c40..7c1516672 100644 --- a/lambdas/redis_sync/tests/test_data/test_read_vaccine_mapping.json +++ b/lambdas/redis_sync/tests/test_data/test_read_vaccine_mapping.json @@ -1,3 +1,3 @@ { - "read": "vaccine_mapping" -} \ No newline at end of file + "read": "vaccine_mapping" +} diff --git a/lambdas/redis_sync/tests/test_handler.py b/lambdas/redis_sync/tests/test_handler.py index 382371684..31b995eae 100644 --- a/lambdas/redis_sync/tests/test_handler.py +++ b/lambdas/redis_sync/tests/test_handler.py @@ -1,4 +1,5 @@ -''' unit tests for redis_sync.py ''' +"""unit tests for redis_sync.py""" + import unittest import importlib from unittest.mock import patch @@ -8,15 +9,15 @@ class TestHandler(unittest.TestCase): s3_vaccine = { - 's3': { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.DISEASE_MAPPING_FILE_KEY} + "s3": { + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.DISEASE_MAPPING_FILE_KEY}, } } s3_supplier = { - 's3': { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY} + "s3": { + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY}, } } @@ -40,60 +41,81 @@ def tearDown(self): self.logger_exception_patcher.stop() def test_handler_success(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} self.mock_get_s3_records.return_value = [self.s3_vaccine] with patch("redis_sync.process_record") as mock_record_processor: - mock_record_processor.return_value = {'status': 'success', 'message': 'Processed successfully', - 'file_key': 'test-key'} + mock_record_processor.return_value = { + "status": "success", + "message": "Processed successfully", + "file_key": "test-key", + } result = redis_sync.handler(mock_event, None) self.assertEqual(result["status"], "success") self.assertEqual(result["message"], "Successfully processed 1 records") - self.assertEqual(result["file_keys"], ['test-key']) + self.assertEqual(result["file_keys"], ["test-key"]) def test_handler_failure(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} with patch("redis_sync.process_record") as mock_record_processor: self.mock_get_s3_records.return_value = [self.s3_vaccine] mock_record_processor.side_effect = Exception("Processing error 1") result = redis_sync.handler(mock_event, None) - self.assertEqual(result, {'status': 'error', 'message': 'Error processing S3 event'}) + self.assertEqual(result, {"status": "error", "message": "Error processing S3 event"}) def test_handler_no_records(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'Records': []} + mock_event = {"Records": []} self.mock_get_s3_records.return_value = [] result = redis_sync.handler(mock_event, None) - self.assertEqual(result, {'status': 'success', 'message': 'No records found in event'}) + self.assertEqual(result, {"status": "success", "message": "No records found in event"}) def test_handler_exception(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} self.mock_get_s3_records.return_value = [self.s3_vaccine] with patch("redis_sync.process_record") as mock_record_processor: mock_record_processor.side_effect = Exception("Processing error 2") result = redis_sync.handler(mock_event, None) - self.assertEqual(result, {'status': 'error', 'message': 'Error processing S3 event'}) + self.assertEqual(result, {"status": "error", "message": "Error processing S3 event"}) def test_handler_with_empty_event(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) self.mock_get_s3_records.return_value = [] result = redis_sync.handler({}, None) - self.assertEqual(result, {'status': 'success', 'message': 'No records found in event'}) + self.assertEqual(result, {"status": "success", "message": "No records found in event"}) def test_handler_multi_record(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'Records': [self.s3_vaccine, self.s3_supplier]} + mock_event = {"Records": [self.s3_vaccine, self.s3_supplier]} # If you need S3EventRecord, uncomment the import and use it here # self.mock_get_s3_records.return_value = [ # S3EventRecord(self.s3_vaccine), @@ -101,22 +123,33 @@ def test_handler_multi_record(self): # ] self.mock_get_s3_records.return_value = [self.s3_vaccine, self.s3_supplier] with patch("redis_sync.process_record") as mock_record_processor: - mock_record_processor.side_effect = [{'status': 'success', 'message': 'Processed successfully', - 'file_key': 'test-key1'}, - {'status': 'success', 'message': 'Processed successfully', - 'file_key': 'test-key2'}] + mock_record_processor.side_effect = [ + { + "status": "success", + "message": "Processed successfully", + "file_key": "test-key1", + }, + { + "status": "success", + "message": "Processed successfully", + "file_key": "test-key2", + }, + ] result = redis_sync.handler(mock_event, None) - self.assertEqual(result['status'], 'success') - self.assertEqual(result['message'], 'Successfully processed 2 records') - self.assertEqual(result['file_keys'][0], 'test-key1') - self.assertEqual(result['file_keys'][1], 'test-key2') + self.assertEqual(result["status"], "success") + self.assertEqual(result["message"], "Successfully processed 2 records") + self.assertEqual(result["file_keys"][0], "test-key1") + self.assertEqual(result["file_keys"][1], "test-key2") def test_handler_read_event(self): - with patch("common.log_decorator.logging_decorator", lambda prefix=None, stream_name=None: (lambda f: f)): + with patch( + "common.log_decorator.logging_decorator", + lambda prefix=None, stream_name=None: (lambda f: f), + ): importlib.reload(redis_sync) - mock_event = {'read': 'myhash'} - mock_read_event_response = {'field1': 'value1'} - with patch('redis_sync.read_event') as mock_read_event: + mock_event = {"read": "myhash"} + mock_read_event_response = {"field1": "value1"} + with patch("redis_sync.read_event") as mock_read_event: mock_read_event.return_value = mock_read_event_response result = redis_sync.handler(mock_event, None) mock_read_event.assert_called_once() diff --git a/lambdas/redis_sync/tests/test_handler_decorator.py b/lambdas/redis_sync/tests/test_handler_decorator.py index 7775d183e..ef2484ac2 100644 --- a/lambdas/redis_sync/tests/test_handler_decorator.py +++ b/lambdas/redis_sync/tests/test_handler_decorator.py @@ -1,4 +1,5 @@ -''' unit tests for redis_sync.py ''' +"""unit tests for redis_sync.py""" + import unittest import json from unittest.mock import patch @@ -8,21 +9,22 @@ class TestHandlerDecorator(unittest.TestCase): - """ Unit tests for the handler decorator in redis_sync.py - these will check what is sent to firehose and the logging - Note: test_handler.py will check the actual business logic of the handler - the decorator is used to log the function execution and send logs to firehose + """Unit tests for the handler decorator in redis_sync.py + these will check what is sent to firehose and the logging + Note: test_handler.py will check the actual business logic of the handler + the decorator is used to log the function execution and send logs to firehose """ + s3_vaccine = { - 's3': { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.DISEASE_MAPPING_FILE_KEY} + "s3": { + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.DISEASE_MAPPING_FILE_KEY}, } } s3_supplier = { - 's3': { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY} + "s3": { + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY}, } } @@ -45,14 +47,16 @@ def tearDown(self): patch.stopall() def test_handler_decorator_success(self): - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} self.mock_get_s3_records.return_value = [self.s3_vaccine] - bucket_name = self.s3_vaccine['s3']['bucket']['name'] - file_key = self.s3_vaccine['s3']['object']['key'] - self.mock_record_processor.return_value = {'status': 'success', - 'message': 'Successfully processed 1 records', - 'bucket_name': bucket_name, - 'file_key': file_key} + bucket_name = self.s3_vaccine["s3"]["bucket"]["name"] + file_key = self.s3_vaccine["s3"]["object"]["key"] + self.mock_record_processor.return_value = { + "status": "success", + "message": "Successfully processed 1 records", + "bucket_name": bucket_name, + "file_key": file_key, + } handler(mock_event, None) @@ -72,16 +76,18 @@ def test_handler_decorator_success(self): self.assertEqual(event["file_keys"], [file_key]) def test_handler_decorator_failure(self): - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} self.mock_get_s3_records.return_value = [self.s3_vaccine] - bucket_name = self.s3_vaccine['s3']['bucket']['name'] - file_key = self.s3_vaccine['s3']['object']['key'] + bucket_name = self.s3_vaccine["s3"]["bucket"]["name"] + file_key = self.s3_vaccine["s3"]["object"]["key"] with patch("redis_sync.process_record") as mock_record_processor: - mock_record_processor.return_value = {'status': 'error', - 'message': 'my-error', - 'bucket_name': bucket_name, - 'file_key': file_key} + mock_record_processor.return_value = { + "status": "error", + "message": "my-error", + "bucket_name": bucket_name, + "file_key": file_key, + } handler(mock_event, None) @@ -96,14 +102,17 @@ def test_handler_decorator_failure(self): self.assertIn("function_name", event) self.assertEqual(event["function_name"], "redis_sync_handler") self.assertEqual(event["status"], "error") - self.assertEqual(event["message"], 'Processed 1 records with 1 errors') + self.assertEqual(event["message"], "Processed 1 records with 1 errors") self.assertEqual(event["file_keys"], [file_key]) def test_handler_decorator_no_records1(self): - mock_event = {'Records': []} + mock_event = {"Records": []} self.mock_get_s3_records.return_value = [] - self.mock_record_processor.return_value = {'status': 'success', 'message': 'No records found in event'} + self.mock_record_processor.return_value = { + "status": "success", + "message": "No records found in event", + } handler(mock_event, None) @@ -123,7 +132,7 @@ def test_handler_decorator_no_records1(self): self.assertNotIn("file_key", event) def test_handler_decorator_exception(self): - mock_event = {'Records': [self.s3_vaccine]} + mock_event = {"Records": [self.s3_vaccine]} self.mock_get_s3_records.side_effect = Exception("Test exception") handler(mock_event, None) @@ -159,17 +168,25 @@ def test_handler_with_empty_event(self): self.assertEqual(event["message"], "No records found in event") def test_handler_multi_record(self): - mock_event = {'Records': [self.s3_vaccine, self.s3_supplier]} + mock_event = {"Records": [self.s3_vaccine, self.s3_supplier]} self.mock_get_s3_records.return_value = [ - S3EventRecord(self.s3_vaccine), S3EventRecord(self.s3_supplier)] + S3EventRecord(self.s3_vaccine), + S3EventRecord(self.s3_supplier), + ] # Mock the return value for each record self.mock_record_processor.side_effect = [ - {'status': 'success', 'message': 'Processed successfully', - 'file_key': RedisCacheKey.DISEASE_MAPPING_FILE_KEY}, - {'status': 'success', 'message': 'Processed successfully', - 'file_key': RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY} + { + "status": "success", + "message": "Processed successfully", + "file_key": RedisCacheKey.DISEASE_MAPPING_FILE_KEY, + }, + { + "status": "success", + "message": "Processed successfully", + "file_key": RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY, + }, ] handler(mock_event, None) @@ -188,12 +205,12 @@ def test_handler_multi_record(self): # test to check that event_read is called when "read" key is passed in the event def test_handler_read_event(self): - mock_event = {'read': 'myhash'} - return_key = 'field1' - return_value = 'value1' + mock_event = {"read": "myhash"} + return_key = "field1" + return_value = "value1" mock_read_event_response = {return_key: return_value} - with patch('redis_sync.read_event') as mock_read_event: + with patch("redis_sync.read_event") as mock_read_event: mock_read_event.return_value = mock_read_event_response handler(mock_event, None) diff --git a/lambdas/redis_sync/tests/test_record_processor.py b/lambdas/redis_sync/tests/test_record_processor.py index d1036ff60..6a3c6f339 100644 --- a/lambdas/redis_sync/tests/test_record_processor.py +++ b/lambdas/redis_sync/tests/test_record_processor.py @@ -8,14 +8,14 @@ class TestRecordProcessor(unittest.TestCase): s3_vaccine = { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.DISEASE_MAPPING_FILE_KEY} + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.DISEASE_MAPPING_FILE_KEY}, } s3_supplier = { - 'bucket': {'name': 'test-bucket1'}, - 'object': {'key': RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY} + "bucket": {"name": "test-bucket1"}, + "object": {"key": RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY}, } - mock_test_file = {'a': 'test', 'b': 'test2'} + mock_test_file = {"a": "test", "b": "test2"} def setUp(self): self.logger_info_patcher = patch("logging.Logger.info") @@ -34,7 +34,7 @@ def tearDown(self): self.redis_cacher_upload_patcher.stop() def test_record_processor_success(self): - """ Test successful processing of a record """ + """Test successful processing of a record""" self.mock_redis_cacher_upload.return_value = {"status": "success"} result = process_record(S3EventRecord(self.s3_vaccine)) @@ -43,7 +43,7 @@ def test_record_processor_success(self): self.assertEqual(result["file_key"], RedisCacheKey.DISEASE_MAPPING_FILE_KEY) def test_record_processor_failure(self): - """ Test failure in processing a record """ + """Test failure in processing a record""" self.mock_redis_cacher_upload.return_value = {"status": "error"} result = process_record(S3EventRecord(self.s3_vaccine)) @@ -52,7 +52,7 @@ def test_record_processor_failure(self): self.assertEqual(result["file_key"], RedisCacheKey.DISEASE_MAPPING_FILE_KEY) def test_record_processor_exception(self): - """ Test exception handling in record processing """ + """Test exception handling in record processing""" msg = "my error msg" self.mock_redis_cacher_upload.side_effect = Exception(msg) @@ -60,5 +60,7 @@ def test_record_processor_exception(self): self.assertEqual(result["status"], "error") self.assertEqual(result["message"], msg) - self.mock_logger_exception.assert_called_once_with("Error uploading to cache for filename '%s'", - RedisCacheKey.DISEASE_MAPPING_FILE_KEY) + self.mock_logger_exception.assert_called_once_with( + "Error uploading to cache for filename '%s'", + RedisCacheKey.DISEASE_MAPPING_FILE_KEY, + ) diff --git a/lambdas/redis_sync/tests/test_redis_cacher.py b/lambdas/redis_sync/tests/test_redis_cacher.py index 98d42c4c0..afdb158cb 100644 --- a/lambdas/redis_sync/tests/test_redis_cacher.py +++ b/lambdas/redis_sync/tests/test_redis_cacher.py @@ -19,7 +19,7 @@ def test_upload(self): mock_data = {"a": "b"} mock_transformed_data = { "vacc_to_diseases": {"b": "c"}, - "diseases_to_vacc": {"c": "b"} + "diseases_to_vacc": {"c": "b"}, } self.mock_s3_reader.read = unittest.mock.Mock() @@ -35,14 +35,20 @@ def test_upload(self): self.mock_redis_client.hmset.assert_any_call("vacc_to_diseases", {"b": "c"}) self.mock_redis_client.hmset.assert_any_call("diseases_to_vacc", {"c": "b"}) self.mock_redis_client.hdel.assert_not_called() - self.assertEqual(result, {"status": "success", "message": f"File {file_key} uploaded to Redis cache."}) + self.assertEqual( + result, + { + "status": "success", + "message": f"File {file_key} uploaded to Redis cache.", + }, + ) def test_deletes_extra_fields(self): mock_data = {"input_key": "input_val"} mock_transformed_data = { "hash_name": { "transformed_key_1": "transformed_val_1", - "transformed_key_2": "transformed_val_2" + "transformed_key_2": "transformed_val_2", }, } @@ -62,12 +68,21 @@ def test_deletes_extra_fields(self): self.mock_s3_reader.read.assert_called_once_with(bucket_name, file_key) self.mock_transform_map.assert_called_once_with(mock_data, file_key) self.mock_redis_client.hgetall.assert_called_once_with("hash_name") - self.mock_redis_client.hmset.assert_called_once_with("hash_name", { - "transformed_key_1": "transformed_val_1", - "transformed_key_2": "transformed_val_2" - }) + self.mock_redis_client.hmset.assert_called_once_with( + "hash_name", + { + "transformed_key_1": "transformed_val_1", + "transformed_key_2": "transformed_val_2", + }, + ) self.mock_redis_client.hdel.assert_called_once_with("hash_name", "obsolete_key_1", "obsolete_key_2") - self.assertEqual(result, {"status": "success", "message": f"File {file_key} uploaded to Redis cache."}) + self.assertEqual( + result, + { + "status": "success", + "message": f"File {file_key} uploaded to Redis cache.", + }, + ) def test_unrecognised_format(self): mock_data = {"a": "b"} @@ -82,7 +97,10 @@ def test_unrecognised_format(self): self.mock_s3_reader.read.assert_called_once_with(bucket_name, file_key) self.assertEqual(result["status"], "warning") - self.assertEqual(result["message"], f"No valid Redis mappings found for file '{file_key}'. Nothing uploaded.") + self.assertEqual( + result["message"], + f"No valid Redis mappings found for file '{file_key}'. Nothing uploaded.", + ) self.mock_logger_warning.assert_called_once() self.mock_redis_client.hmset.assert_not_called() self.mock_redis_client.hdel.assert_not_called() diff --git a/lambdas/redis_sync/tests/test_transform_config.py b/lambdas/redis_sync/tests/test_transform_config.py index 968ae0437..4aa3b18c7 100644 --- a/lambdas/redis_sync/tests/test_transform_config.py +++ b/lambdas/redis_sync/tests/test_transform_config.py @@ -1,9 +1,10 @@ - import unittest import json from unittest.mock import patch from transform_configs import ( - transform_vaccine_map, transform_supplier_permissions, transform_validation_rules + transform_vaccine_map, + transform_supplier_permissions, + transform_validation_rules, ) @@ -53,7 +54,10 @@ def test_validation_rules(self): def test_empty_input(self): result = transform_supplier_permissions([]) - self.assertEqual(result, { - "supplier_permissions": {}, - "ods_code_to_supplier": {}, - }) + self.assertEqual( + result, + { + "supplier_permissions": {}, + "ods_code_to_supplier": {}, + }, + ) diff --git a/lambdas/redis_sync/tests/test_transform_map.py b/lambdas/redis_sync/tests/test_transform_map.py index 56992a18b..e0279f301 100644 --- a/lambdas/redis_sync/tests/test_transform_map.py +++ b/lambdas/redis_sync/tests/test_transform_map.py @@ -1,4 +1,3 @@ - import unittest from unittest.mock import patch from transform_map import transform_map @@ -9,8 +8,10 @@ class TestTransformMap(unittest.TestCase): def setUp(self): self.mock_logger_info = patch("transform_map.logger.info").start() self.mock_logger_warning = patch("transform_map.logger.warning").start() - self.mock_supplier_permissions = patch("transform_map.transform_supplier_permissions", - return_value={"result": "supplier"}).start() + self.mock_supplier_permissions = patch( + "transform_map.transform_supplier_permissions", + return_value={"result": "supplier"}, + ).start() self.mock_vaccine_map = patch("transform_map.transform_vaccine_map", return_value={"result": "vaccine"}).start() self.mock_validation_rules = patch("transform_map.transform_validation_rules").start() @@ -23,7 +24,9 @@ def test_permissions_config_file_key_calls_supplier_permissions(self): self.mock_supplier_permissions.assert_called_once_with(data) self.assertEqual(result, {"result": "supplier"}) self.mock_logger_info.assert_any_call( - "Transforming data for file type: %s", RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY) + "Transforming data for file type: %s", + RedisCacheKey.PERMISSIONS_CONFIG_FILE_KEY, + ) def test_disease_mapping_file_key_calls_vaccine_map(self): data = {"other": "data"} @@ -32,7 +35,9 @@ def test_disease_mapping_file_key_calls_vaccine_map(self): self.mock_vaccine_map.assert_called_once_with(data) self.assertEqual(result, {"result": "vaccine"}) self.mock_logger_info.assert_any_call( - "Transforming data for file type: %s", RedisCacheKey.DISEASE_MAPPING_FILE_KEY) + "Transforming data for file type: %s", + RedisCacheKey.DISEASE_MAPPING_FILE_KEY, + ) def test_validation_rules_file_key_calls_validation_rules(self): data = {"validation": "schema"} @@ -41,4 +46,6 @@ def test_validation_rules_file_key_calls_validation_rules(self): self.mock_validation_rules.assert_called_once_with(data) self.assertEqual(result, {"validation_rules": data}) self.mock_logger_info.assert_any_call( - "Transforming data for file type: %s", RedisCacheKey.VALIDATION_RULES_FILE_KEY) + "Transforming data for file type: %s", + RedisCacheKey.VALIDATION_RULES_FILE_KEY, + ) diff --git a/lambdas/shared/README.md b/lambdas/shared/README.md index 4b909c893..cc7954089 100644 --- a/lambdas/shared/README.md +++ b/lambdas/shared/README.md @@ -7,6 +7,7 @@ ## Purpose This shared library promotes: + - **Code Reusability:** Common functionality used across multiple Lambda functions - **Consistency:** Standardized patterns for AWS service interactions - **Maintainability:** Centralized location for shared utilities and configurations @@ -188,6 +189,7 @@ Common environment variables used by shared components: ## Dependencies See `requirements.txt` for Python package dependencies: + - `boto3` - AWS SDK - `botocore` - AWS core library - Additional utilities as needed @@ -207,4 +209,4 @@ See `requirements.txt` for Python package dependencies: ## License -This project is maintained by NHS. See [LICENSE](../LICENSE) for details. \ No newline at end of file +This project is maintained by NHS. See [LICENSE](../LICENSE) for details. diff --git a/lambdas/shared/src/common/authentication.py b/lambdas/shared/src/common/authentication.py index 899bf2152..d9cb75284 100644 --- a/lambdas/shared/src/common/authentication.py +++ b/lambdas/shared/src/common/authentication.py @@ -23,17 +23,23 @@ def __init__(self, service: Service, secret_manager_client, environment, cache: self.cache_key = f"{service.value}_access_token" self.expiry = 30 - self.secret_name = f"imms/pds/{environment}/jwt-secrets" if service == Service.PDS else \ - f"imms/immunization/{environment}/jwt-secrets" + self.secret_name = ( + f"imms/pds/{environment}/jwt-secrets" + if service == Service.PDS + else f"imms/immunization/{environment}/jwt-secrets" + ) - self.token_url = f"https://{environment}.api.service.nhs.uk/oauth2/token" \ - if environment != "prod" else "https://api.service.nhs.uk/oauth2/token" + self.token_url = ( + f"https://{environment}.api.service.nhs.uk/oauth2/token" + if environment != "prod" + else "https://api.service.nhs.uk/oauth2/token" + ) def get_service_secrets(self): kwargs = {"SecretId": self.secret_name} response = self.secret_manager_client.get_secret_value(**kwargs) - secret_object = json.loads(response['SecretString']) - secret_object['private_key'] = base64.b64decode(secret_object['private_key_b64']).decode() + secret_object = json.loads(response["SecretString"]) + secret_object["private_key"] = base64.b64decode(secret_object["private_key_b64"]).decode() return secret_object @@ -41,19 +47,19 @@ def create_jwt(self, now: int): logger.info("create_jwt") secret_object = self.get_service_secrets() claims = { - "iss": secret_object['api_key'], - "sub": secret_object['api_key'], + "iss": secret_object["api_key"], + "sub": secret_object["api_key"], "aud": self.token_url, "iat": now, "exp": now + self.expiry, - "jti": str(uuid.uuid4()) + "jti": str(uuid.uuid4()), } return jwt.encode( claims, - secret_object['private_key'], - algorithm='RS512', - headers={"kid": secret_object['kid']} + secret_object["private_key"], + algorithm="RS512", + headers={"kid": secret_object["kid"]}, ) def get_access_token(self): @@ -72,19 +78,17 @@ def get_access_token(self): logger.info("No valid cached token found, creating new token") _jwt = self.create_jwt(now) - headers = { - 'Content-Type': 'application/x-www-form-urlencoded' - } + headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { - 'grant_type': 'client_credentials', - 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - 'client_assertion': _jwt + "grant_type": "client_credentials", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": _jwt, } token_response = requests.post(self.token_url, data=data, headers=headers) if token_response.status_code != 200: raise UnhandledResponseError(response=token_response.text, message="Failed to get access token") - token = token_response.json().get('access_token') + token = token_response.json().get("access_token") self.cache.put(self.cache_key, {"token": token, "expires_at": now + self.expiry}) diff --git a/lambdas/shared/src/common/aws_lambda_event.py b/lambdas/shared/src/common/aws_lambda_event.py index c5836de3c..69eb0dbe1 100644 --- a/lambdas/shared/src/common/aws_lambda_event.py +++ b/lambdas/shared/src/common/aws_lambda_event.py @@ -14,10 +14,10 @@ class AwsLambdaEvent: def __init__(self, event: Dict[str, Any]): self.event_source = None self.event_type = AwsEventType.UNKNOWN - self.event_source = event.get('eventSource') + self.event_source = event.get("eventSource") if self.event_source in [e.value for e in AwsEventType]: self.event_type = AwsEventType(self.event_source) self.records = [] if "Records" in event: - self.records = event.get('Records', []) + self.records = event.get("Records", []) diff --git a/lambdas/shared/src/common/log_decorator.py b/lambdas/shared/src/common/log_decorator.py index e549978d2..725f597e0 100644 --- a/lambdas/shared/src/common/log_decorator.py +++ b/lambdas/shared/src/common/log_decorator.py @@ -1,8 +1,9 @@ """This module contains the logging decorator for sending the appropriate logs to Cloudwatch and Firehose. - The decorator log pattern is shared by filenameprocessor, recordprocessor, ack_backend and id_sync modules. - and therefore could be moved to a common module in the future. - TODO: Duplication check has been suppressed in sonar-project.properties. Remove once refactored. +The decorator log pattern is shared by filenameprocessor, recordprocessor, ack_backend and id_sync modules. +and therefore could be moved to a common module in the future. +TODO: Duplication check has been suppressed in sonar-project.properties. Remove once refactored. """ + import json import time from datetime import datetime @@ -20,17 +21,26 @@ def send_log_to_firehose(stream_name, log_data: dict) -> None: logger.exception("Error sending log to Firehose: %s", error) -def generate_and_send_logs(stream_name, - start_time, base_log_data: dict, additional_log_data: dict, - use_ms_precision: bool = False, is_error_log: bool = False - ) -> None: +def generate_and_send_logs( + stream_name, + start_time, + base_log_data: dict, + additional_log_data: dict, + use_ms_precision: bool = False, + is_error_log: bool = False, +) -> None: """Generates log data which includes the base_log_data, additional_log_data, and time taken (calculated using the current time and given start_time) and sends them to Cloudwatch and Firehose.""" seconds_elapsed = time.time() - start_time - formatted_time_elapsed = f"{round(seconds_elapsed * 1000, 5)}ms" if use_ms_precision else \ - f"{round(seconds_elapsed, 5)}s" + formatted_time_elapsed = ( + f"{round(seconds_elapsed * 1000, 5)}ms" if use_ms_precision else f"{round(seconds_elapsed, 5)}s" + ) - log_data = {**base_log_data, "time_taken": formatted_time_elapsed, **additional_log_data} + log_data = { + **base_log_data, + "time_taken": formatted_time_elapsed, + **additional_log_data, + } log_function = logger.error if is_error_log else logger.info log_function(json.dumps(log_data)) send_log_to_firehose(stream_name, log_data) @@ -43,18 +53,24 @@ def wrapper(*args, **kwargs): logger.info("Starting function: %s", func.__name__) base_log_data = { "function_name": f"{prefix}_{func.__name__}", - "date_time": str(datetime.now()) + "date_time": str(datetime.now()), } start_time = time.time() try: result = func(*args, **kwargs) - generate_and_send_logs(stream_name, - start_time, base_log_data, additional_log_data=result) + generate_and_send_logs(stream_name, start_time, base_log_data, additional_log_data=result) return result except Exception as e: additional_log_data = {"statusCode": 500, "error": str(e)} - generate_and_send_logs(stream_name, - start_time, base_log_data, additional_log_data, is_error_log=True) + generate_and_send_logs( + stream_name, + start_time, + base_log_data, + additional_log_data, + is_error_log=True, + ) raise + return wrapper + return decorator diff --git a/lambdas/shared/src/common/pds_service.py b/lambdas/shared/src/common/pds_service.py index 4eedfae44..6bdeeaa38 100644 --- a/lambdas/shared/src/common/pds_service.py +++ b/lambdas/shared/src/common/pds_service.py @@ -11,8 +11,11 @@ def __init__(self, authenticator: AppRestrictedAuth, environment): logger.info(f"PdsService init: {environment}") self.authenticator = authenticator - self.base_url = f"https://{environment}.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient" \ - if environment != "prod" else "https://api.service.nhs.uk/personal-demographics/FHIR/R4/Patient" + self.base_url = ( + f"https://{environment}.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient" + if environment != "prod" + else "https://api.service.nhs.uk/personal-demographics/FHIR/R4/Patient" + ) logger.info(f"PDS Service URL: {self.base_url}") @@ -20,9 +23,9 @@ def get_patient_details(self, patient_id) -> dict | None: logger.info(f"PDS. Get patient details for ID: {patient_id}") access_token = self.authenticator.get_access_token() request_headers = { - 'Authorization': f'Bearer {access_token}', - 'X-Request-ID': str(uuid.uuid4()), - 'X-Correlation-ID': str(uuid.uuid4()) + "Authorization": f"Bearer {access_token}", + "X-Request-ID": str(uuid.uuid4()), + "X-Correlation-ID": str(uuid.uuid4()), } response = requests.get(f"{self.base_url}/{patient_id}", headers=request_headers, timeout=5) diff --git a/lambdas/shared/src/common/s3_event.py b/lambdas/shared/src/common/s3_event.py index 7dc4b098f..90d8eef05 100644 --- a/lambdas/shared/src/common/s3_event.py +++ b/lambdas/shared/src/common/s3_event.py @@ -9,15 +9,16 @@ class S3EventRecord: - `S3EventRecord` provides access to individual record fields. - `S3Event` wraps the event and extracts a list of `S3EventRecord` objects. """ + def __init__(self, s3_record): self.s3_record = s3_record def get_bucket_name(self): - bucket = self.s3_record['bucket'] - return bucket.get('name') + bucket = self.s3_record["bucket"] + return bucket.get("name") def get_object_key(self): - ret = self.s3_record['object']['key'] + ret = self.s3_record["object"]["key"] return ret @@ -27,4 +28,4 @@ def __init__(self, event): def get_s3_records(self): # return a list of S3EventRecord objects - stripping out the s3 key - return [S3EventRecord(record['s3']) for record in self.records] + return [S3EventRecord(record["s3"]) for record in self.records] diff --git a/lambdas/shared/src/common/s3_reader.py b/lambdas/shared/src/common/s3_reader.py index 2f740956a..28cd00e97 100644 --- a/lambdas/shared/src/common/s3_reader.py +++ b/lambdas/shared/src/common/s3_reader.py @@ -2,7 +2,6 @@ class S3Reader: - """ Fetch the file from S3 using the specified bucket and key. The file is expected to be a UTF-8 encoded text file (e.g., JSON or plain text). diff --git a/lambdas/shared/tests/test_common/test_authentication.py b/lambdas/shared/tests/test_common/test_authentication.py index c0e2afd87..728601e3e 100644 --- a/lambdas/shared/tests/test_common/test_authentication.py +++ b/lambdas/shared/tests/test_common/test_authentication.py @@ -18,7 +18,11 @@ def setUp(self): # The private key must be stored as base64 encoded in secret-manager b64_private_key = base64.b64encode(self.private_key.encode()).decode() - pds_secret = {"private_key_b64": b64_private_key, "kid": self.kid, "api_key": self.api_key} + pds_secret = { + "private_key_b64": b64_private_key, + "kid": self.kid, + "api_key": self.api_key, + } secret_response = {"SecretString": json.dumps(pds_secret)} self.secret_manager_client = MagicMock() @@ -36,13 +40,18 @@ def test_post_request_to_token(self): """it should send a POST request to oauth2 service""" _jwt = "a-jwt" request_data = { - 'grant_type': 'client_credentials', - 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - 'client_assertion': _jwt + "grant_type": "client_credentials", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": _jwt, } access_token = "an-access-token" - responses.add(responses.POST, self.url, status=200, json={"access_token": access_token}, - match=[matchers.urlencoded_params_matcher(request_data)]) + responses.add( + responses.POST, + self.url, + status=200, + json={"access_token": access_token}, + match=[matchers.urlencoded_params_matcher(request_data)], + ) with patch("common.authentication.jwt.encode") as mock_jwt: mock_jwt.return_value = _jwt @@ -61,7 +70,7 @@ def test_jwt_values(self): "aud": self.url, "iat": ANY, "exp": ANY, - "jti": ANY + "jti": ANY, } _jwt = "a-jwt" access_token = "an-access-token" @@ -73,8 +82,7 @@ def test_jwt_values(self): # When self.authenticator.get_access_token() # Then - mock_jwt.assert_called_once_with(claims, self.private_key, - algorithm="RS512", headers={"kid": self.kid}) + mock_jwt.assert_called_once_with(claims, self.private_key, algorithm="RS512", headers={"kid": self.kid}) def test_env_mapping(self): """it should target int environment for none-prod environment, otherwise int""" @@ -92,7 +100,7 @@ def test_returned_cached_token(self): """it should return cached token""" cached_token = { "token": "a-cached-access-token", - "expires_at": int(time.time()) + 99999 # make sure it's not expired + "expires_at": int(time.time()) + 99999, # make sure it's not expired } self.cache.get.return_value = cached_token @@ -108,10 +116,7 @@ def test_update_cache(self): """it should update cached token""" self.cache.get.return_value = None token = "a-new-access-token" - cached_token = { - "token": token, - "expires_at": ANY - } + cached_token = {"token": token, "expires_at": ANY} responses.add(responses.POST, self.url, status=200, json={"access_token": token}) with patch("jwt.encode") as mock_jwt: @@ -147,7 +152,7 @@ def test_expired_token_in_cache(self): # Then exp_cached_token = { "token": new_token, - "expires_at": new_now + self.authenticator.expiry + "expires_at": new_now + self.authenticator.expiry, } self.cache.put.assert_called_once_with(ANY, exp_cached_token) diff --git a/lambdas/shared/tests/test_common/test_aws_dynamodb.py b/lambdas/shared/tests/test_common/test_aws_dynamodb.py index 1775eb25f..7575d9156 100644 --- a/lambdas/shared/tests/test_common/test_aws_dynamodb.py +++ b/lambdas/shared/tests/test_common/test_aws_dynamodb.py @@ -17,11 +17,9 @@ def setUp(self): self.getenv_patch = patch("os.getenv") self.mock_getenv = self.getenv_patch.start() - self.mock_getenv.side_effect = lambda key, default=None: { - "AWS_REGION": self.AWS_REGION - }.get(key, default) + self.mock_getenv.side_effect = lambda key, default=None: {"AWS_REGION": self.AWS_REGION}.get(key, default) - self.dynamodb_resource_patcher = patch('common.aws_dynamodb.dynamodb_resource') + self.dynamodb_resource_patcher = patch("common.aws_dynamodb.dynamodb_resource") self.mock_dynamodb_resource = self.dynamodb_resource_patcher.start() def tearDown(self): diff --git a/lambdas/shared/tests/test_common/test_aws_lambda_event.py b/lambdas/shared/tests/test_common/test_aws_lambda_event.py index 09de4400f..3c1f73142 100644 --- a/lambdas/shared/tests/test_common/test_aws_lambda_event.py +++ b/lambdas/shared/tests/test_common/test_aws_lambda_event.py @@ -7,55 +7,50 @@ class TestAwsLambdaEvent(unittest.TestCase): def setUp(self): """Set up test fixtures""" self.sqs_record_dict = { - 'messageId': '12345-abcde-67890', - 'receiptHandle': 'AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...', - 'body': '{"key": "value"}', - 'attributes': { - 'ApproximateReceiveCount': '1', - 'SentTimestamp': '1545082649183' + "messageId": "12345-abcde-67890", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": '{"key": "value"}', + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1545082649183", }, - 'messageAttributes': {}, - 'md5OfBody': 'e4e68fb7bd0e697a0ae8f1bb342846b3', - 'eventSource': 'aws:sqs', - 'eventSourceARN': 'arn:aws:sqs:us-east-1:123456789012:my-queue', - 'awsRegion': 'us-east-1' + "messageAttributes": {}, + "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:my-queue", + "awsRegion": "us-east-1", } def test_init_with_sqs_event(self): """Test initialization with SQS event""" - event = { - 'Records': [self.sqs_record_dict], - 'eventSource': 'aws:sqs' - } + event = {"Records": [self.sqs_record_dict], "eventSource": "aws:sqs"} lambda_event = AwsLambdaEvent(event) self.assertEqual(lambda_event.event_type, AwsEventType.SQS) self.assertEqual(len(lambda_event.records), 1) - self.assertEqual(lambda_event.records[0]['messageId'], '12345-abcde-67890') + self.assertEqual(lambda_event.records[0]["messageId"], "12345-abcde-67890") def test_init_with_multiple_sqs_records(self): """Test initialization with multiple SQS records""" sqs_record_2 = self.sqs_record_dict.copy() - sqs_record_2['messageId'] = 'second-message-id' + sqs_record_2["messageId"] = "second-message-id" event = { - 'Records': [self.sqs_record_dict, sqs_record_2], - 'eventSource': 'aws:sqs' + "Records": [self.sqs_record_dict, sqs_record_2], + "eventSource": "aws:sqs", } lambda_event = AwsLambdaEvent(event) self.assertEqual(lambda_event.event_type, AwsEventType.SQS) self.assertEqual(len(lambda_event.records), 2) - self.assertEqual(lambda_event.records[0]['messageId'], '12345-abcde-67890') - self.assertEqual(lambda_event.records[1]['messageId'], 'second-message-id') + self.assertEqual(lambda_event.records[0]["messageId"], "12345-abcde-67890") + self.assertEqual(lambda_event.records[1]["messageId"], "second-message-id") def test_init_with_empty_records(self): """Test initialization with empty records array""" - event = { - 'Records': [] - } + event = {"Records": []} lambda_event = AwsLambdaEvent(event) @@ -64,9 +59,7 @@ def test_init_with_empty_records(self): def test_init_without_records(self): """Test initialization without Records key""" - event = { - 'some_other_key': 'value' - } + event = {"some_other_key": "value"} lambda_event = AwsLambdaEvent(event) @@ -75,13 +68,8 @@ def test_init_without_records(self): def test_init_with_unknown_event_source(self): """Test initialization with unknown event source""" - unknown_record = { - 'eventSource': 'aws:unknown-service', - 'data': 'test' - } - event = { - 'Records': [unknown_record] - } + unknown_record = {"eventSource": "aws:unknown-service", "data": "test"} + event = {"Records": [unknown_record]} lambda_event = AwsLambdaEvent(event) @@ -90,13 +78,8 @@ def test_init_with_unknown_event_source(self): def test_init_with_missing_event_source(self): """Test initialization with record missing eventSource""" - record_without_source = { - 'messageId': 'test-id', - 'body': 'test' - } - event = { - 'Records': [record_without_source] - } + record_without_source = {"messageId": "test-id", "body": "test"} + event = {"Records": [record_without_source]} lambda_event = AwsLambdaEvent(event) @@ -112,7 +95,7 @@ def test_enum_values(self): def test_mixed_multiple_records(self): """Test that mixed event sources uses the first record's type""" mixed_records = [self.sqs_record_dict, {}] - event = {'Records': mixed_records, 'eventSource': 'aws:sqs'} + event = {"Records": mixed_records, "eventSource": "aws:sqs"} lambda_event = AwsLambdaEvent(event) @@ -121,7 +104,7 @@ def test_mixed_multiple_records(self): def test_empty_records(self): """Test empty records""" - event = {'Records': []} + event = {"Records": []} lambda_event = AwsLambdaEvent(event) diff --git a/lambdas/shared/tests/test_common/test_clients.py b/lambdas/shared/tests/test_common/test_clients.py index f5f4f6032..561e9c9d6 100644 --- a/lambdas/shared/tests/test_common/test_clients.py +++ b/lambdas/shared/tests/test_common/test_clients.py @@ -61,12 +61,12 @@ def test_logger_set_level(self): self.mock_logger_instance.setLevel.assert_called_once_with(logging.INFO) def test_global_s3_client(self): - ''' Test global_s3_client is not initialized on import ''' + """Test global_s3_client is not initialized on import""" importlib.reload(clients) self.assertEqual(clients.global_s3_client, None) def test_global_s3_client_initialization(self): - ''' Test global_s3_client is initialized exactly once even with multiple invocations''' + """Test global_s3_client is initialized exactly once even with multiple invocations""" importlib.reload(clients) clients.get_s3_client() self.assertNotEqual(clients.global_s3_client, None) diff --git a/lambdas/shared/tests/test_common/test_errors.py b/lambdas/shared/tests/test_common/test_errors.py index 778addd21..aae4d66e5 100644 --- a/lambdas/shared/tests/test_common/test_errors.py +++ b/lambdas/shared/tests/test_common/test_errors.py @@ -21,7 +21,7 @@ def assert_resource_type_and_id(self, context, resource_type, resource_id): self.assertEqual(context.exception.resource_id, resource_id) def assert_operation_outcome(self, outcome): - self.assertEqual(outcome.get('resourceType'), "OperationOutcome") + self.assertEqual(outcome.get("resourceType"), "OperationOutcome") def test_errors_unauthorized_error(self): """Test correct operation of UnauthorizedError""" @@ -34,10 +34,10 @@ def test_errors_unauthorized_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.forbidden) - self.assertEqual(issue.get('diagnostics'), "Unauthorized request") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.forbidden) + self.assertEqual(issue.get("diagnostics"), "Unauthorized request") def test_errors_unauthorized_vax_error(self): """Test correct operation of UnauthorizedVaxError""" @@ -50,10 +50,10 @@ def test_errors_unauthorized_vax_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.forbidden) - self.assertEqual(issue.get('diagnostics'), "Unauthorized request for vaccine type") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.forbidden) + self.assertEqual(issue.get("diagnostics"), "Unauthorized request for vaccine type") def test_errors_unauthorized_vax_on_record_error(self): """Test correct operation of UnauthorizedVaxOnRecordError""" @@ -66,12 +66,12 @@ def test_errors_unauthorized_vax_on_record_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.forbidden) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.forbidden) self.assertEqual( - issue.get('diagnostics'), - "Unauthorized request for vaccine type present in the stored immunization resource" + issue.get("diagnostics"), + "Unauthorized request for vaccine type present in the stored immunization resource", ) def test_errors_token_validation_error(self): @@ -85,10 +85,10 @@ def test_errors_token_validation_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.invalid) - self.assertEqual(issue.get('diagnostics'), "Missing/Invalid Token") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.invalid) + self.assertEqual(issue.get("diagnostics"), "Missing/Invalid Token") def test_errors_conflict_error(self): """Test correct operation of ConflictError""" @@ -101,10 +101,10 @@ def test_errors_conflict_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.duplicate) - self.assertEqual(issue.get('diagnostics'), "Conflict") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.duplicate) + self.assertEqual(issue.get("diagnostics"), "Conflict") def test_errors_resource_not_found_error(self): """Test correct operation of ResourceNotFoundError""" @@ -116,16 +116,16 @@ def test_errors_resource_not_found_error(self): self.assert_resource_type_and_id(context, test_resource_type, test_resource_id) self.assertEqual( str(context.exception), - f"{test_resource_type} resource does not exist. ID: {test_resource_id}" + f"{test_resource_type} resource does not exist. ID: {test_resource_id}", ) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.not_found) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.not_found) self.assertEqual( - issue.get('diagnostics'), - f"{test_resource_type} resource does not exist. ID: {test_resource_id}" + issue.get("diagnostics"), + f"{test_resource_type} resource does not exist. ID: {test_resource_id}", ) def test_errors_resource_found_error(self): @@ -138,16 +138,16 @@ def test_errors_resource_found_error(self): self.assert_resource_type_and_id(context, test_resource_type, test_resource_id) self.assertEqual( str(context.exception), - f"{test_resource_type} resource does exist. ID: {test_resource_id}" + f"{test_resource_type} resource does exist. ID: {test_resource_id}", ) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.not_found) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.not_found) self.assertEqual( - issue.get('diagnostics'), - f"{test_resource_type} resource does exist. ID: {test_resource_id}" + issue.get("diagnostics"), + f"{test_resource_type} resource does exist. ID: {test_resource_id}", ) def test_errors_unhandled_response_error(self): @@ -161,10 +161,10 @@ def test_errors_unhandled_response_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.exception) - self.assertEqual(issue.get('diagnostics'), f"{test_message}\n{test_response}") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.exception) + self.assertEqual(issue.get("diagnostics"), f"{test_message}\n{test_response}") def test_errors_bad_request_error(self): """Test correct operation of BadRequestError""" @@ -177,10 +177,10 @@ def test_errors_bad_request_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.incomplete) - self.assertEqual(issue.get('diagnostics'), f"{test_message}\n{test_response}") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.incomplete) + self.assertEqual(issue.get("diagnostics"), f"{test_message}\n{test_response}") def test_errors_mandatory_error(self): """Test correct operation of MandatoryError""" @@ -213,16 +213,16 @@ def test_errors_invalid_patient_id(self): self.assertEqual(context.exception.patient_identifier, test_patient_identifier) self.assertEqual( str(context.exception), - f"NHS Number: {test_patient_identifier} is invalid or it doesn't exist." + f"NHS Number: {test_patient_identifier} is invalid or it doesn't exist.", ) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.exception) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.exception) self.assertEqual( - issue.get('diagnostics'), - f"NHS Number: {test_patient_identifier} is invalid or it doesn't exist." + issue.get("diagnostics"), + f"NHS Number: {test_patient_identifier} is invalid or it doesn't exist.", ) def test_errors_inconsistent_id_error(self): @@ -234,16 +234,16 @@ def test_errors_inconsistent_id_error(self): self.assertEqual(context.exception.imms_id, test_imms_id) self.assertEqual( str(context.exception), - f"The provided id:{test_imms_id} doesn't match with the content of the message" + f"The provided id:{test_imms_id} doesn't match with the content of the message", ) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.exception) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.exception) self.assertEqual( - issue.get('diagnostics'), - f"The provided id:{test_imms_id} doesn't match with the content of the message" + issue.get("diagnostics"), + f"The provided id:{test_imms_id} doesn't match with the content of the message", ) def test_errors_custom_validation_error(self): @@ -256,10 +256,10 @@ def test_errors_custom_validation_error(self): self.assertEqual(str(context.exception), test_message) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.invariant) - self.assertEqual(issue.get('diagnostics'), test_message) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.invariant) + self.assertEqual(issue.get("diagnostics"), test_message) def test_errors_identifier_duplication_error(self): """Test correct operation of IdentifierDuplicationError""" @@ -270,16 +270,16 @@ def test_errors_identifier_duplication_error(self): self.assertEqual(context.exception.identifier, test_identifier) self.assertEqual( str(context.exception), - f"The provided identifier: {test_identifier} is duplicated" + f"The provided identifier: {test_identifier} is duplicated", ) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.duplicate) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.duplicate) self.assertEqual( - issue.get('diagnostics'), - f"The provided identifier: {test_identifier} is duplicated" + issue.get("diagnostics"), + f"The provided identifier: {test_identifier} is duplicated", ) def test_errors_server_error(self): @@ -293,10 +293,10 @@ def test_errors_server_error(self): self.assertEqual(str(context.exception), f"{test_message}\n{test_response}") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.server_error) - self.assertEqual(issue.get('diagnostics'), f"{test_message}\n{test_response}") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.server_error) + self.assertEqual(issue.get("diagnostics"), f"{test_message}\n{test_response}") def test_errors_parameter_exception(self): """Test correct operation of ParameterException""" @@ -317,10 +317,10 @@ def test_errors_unauthorized_system_error(self): self.assertEqual(str(context.exception), test_message) outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.forbidden) - self.assertEqual(issue.get('diagnostics'), test_message) + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.forbidden) + self.assertEqual(issue.get("diagnostics"), test_message) def test_errors_unauthorized_system_error_no_message(self): """Test correct operation of UnauthorizedSystemError with no message""" @@ -331,10 +331,10 @@ def test_errors_unauthorized_system_error_no_message(self): self.assertEqual(str(context.exception), "Unauthorized system") outcome = context.exception.to_operation_outcome() self.assert_operation_outcome(outcome) - issue = outcome.get('issue')[0] - self.assertEqual(issue.get('severity'), errors.Severity.error) - self.assertEqual(issue.get('code'), errors.Code.forbidden) - self.assertEqual(issue.get('diagnostics'), "Unauthorized system") + issue = outcome.get("issue")[0] + self.assertEqual(issue.get("severity"), errors.Severity.error) + self.assertEqual(issue.get("code"), errors.Code.forbidden) + self.assertEqual(issue.get("diagnostics"), "Unauthorized system") def test_errors_message_not_successful_error(self): """Test correct operation of MessageNotSuccessfulError""" @@ -353,9 +353,7 @@ def test_errors_message_not_successful_error_no_message(self): def test_errors_record_processor_error(self): """Test correct operation of RecordProcessorError""" - test_diagnostics = { - "test_diagnostic": "test_value" - } + test_diagnostics = {"test_diagnostic": "test_value"} with self.assertRaises(errors.RecordProcessorError) as context: raise errors.RecordProcessorError(test_diagnostics) diff --git a/lambdas/shared/tests/test_common/test_log_decorator.py b/lambdas/shared/tests/test_common/test_log_decorator.py index 9ada72bb3..5731762bb 100644 --- a/lambdas/shared/tests/test_common/test_log_decorator.py +++ b/lambdas/shared/tests/test_common/test_log_decorator.py @@ -3,7 +3,11 @@ import json from datetime import datetime -from common.log_decorator import logging_decorator, generate_and_send_logs, send_log_to_firehose +from common.log_decorator import ( + logging_decorator, + generate_and_send_logs, + send_log_to_firehose, +) class TestLogDecorator(unittest.TestCase): @@ -38,8 +42,7 @@ def test_send_log_to_firehose_success(self): # Assert expected_record = {"Data": json.dumps({"event": test_log_data}).encode("utf-8")} self.mock_firehose_client.put_record.assert_called_once_with( - DeliveryStreamName=self.test_stream, - Record=expected_record + DeliveryStreamName=self.test_stream, Record=expected_record ) def test_send_log_to_firehose_exception(self): @@ -55,7 +58,7 @@ def test_send_log_to_firehose_exception(self): self.mock_firehose_client.put_record.assert_called_once() self.mock_logger_exception.assert_called_once_with( "Error sending log to Firehose: %s", - self.mock_firehose_client.put_record.side_effect + self.mock_firehose_client.put_record.side_effect, ) @patch("time.time") @@ -77,7 +80,7 @@ def test_generate_and_send_logs_success(self, mock_send_log, mock_time): "date_time": "2023-01-01", "time_taken": "0.5s", "statusCode": 200, - "result": "success" + "result": "success", } self.mock_logger_error.assert_not_called() mock_send_log.assert_called_once_with(self.test_stream, expected_log_data) @@ -93,7 +96,13 @@ def test_generate_and_send_logs_with_ms_precision(self, mock_send_log, mock_time additional_log_data = {"statusCode": 200, "result": "success"} # Act - generate_and_send_logs(self.test_stream, start_time, base_log_data, additional_log_data, use_ms_precision=True) + generate_and_send_logs( + self.test_stream, + start_time, + base_log_data, + additional_log_data, + use_ms_precision=True, + ) # Assert expected_log_data = { @@ -101,7 +110,7 @@ def test_generate_and_send_logs_with_ms_precision(self, mock_send_log, mock_time "date_time": "2023-01-01", "time_taken": "500.0ms", "statusCode": 200, - "result": "success" + "result": "success", } self.mock_logger_error.assert_not_called() mock_send_log.assert_called_once_with(self.test_stream, expected_log_data) @@ -117,7 +126,13 @@ def test_generate_and_send_logs_error(self, mock_send_log, mock_time): additional_log_data = {"statusCode": 500, "error": "Test error"} # Act - generate_and_send_logs(self.test_stream, start_time, base_log_data, additional_log_data, is_error_log=True) + generate_and_send_logs( + self.test_stream, + start_time, + base_log_data, + additional_log_data, + is_error_log=True, + ) # Assert expected_log_data = { @@ -125,7 +140,7 @@ def test_generate_and_send_logs_error(self, mock_send_log, mock_time): "date_time": "2023-01-01", "time_taken": "0.75s", "statusCode": 500, - "error": "Test error" + "error": "Test error", } self.mock_logger_error.assert_called_once_with(json.dumps(expected_log_data)) mock_send_log.assert_called_once_with(self.test_stream, expected_log_data) @@ -156,7 +171,7 @@ def test_function(x, y): self.assertEqual(call_args[0], self.test_stream) # stream_name self.assertEqual(call_args[1], 1000.0) # start_time self.assertEqual(call_args[2]["function_name"], f"{self.test_prefix}_test_function") # base_log_data - self.assertEqual(call_kwargs['additional_log_data'], {"statusCode": 200, "result": 5}) # additional_log_data + self.assertEqual(call_kwargs["additional_log_data"], {"statusCode": 200, "result": 5}) # additional_log_data self.assertNotIn("is_error_log", call_kwargs) # Should not be error log @patch("common.log_decorator.time") @@ -183,7 +198,10 @@ def test_function_with_error(): self.assertEqual(call_args[0], self.test_stream) # stream_name self.assertEqual(call_args[1], 1000.0) # start_time - self.assertEqual(call_args[2]["function_name"], f"{self.test_prefix}_test_function_with_error") # base_log_data + self.assertEqual( + call_args[2]["function_name"], + f"{self.test_prefix}_test_function_with_error", + ) # base_log_data self.assertEqual(call_args[3], {"statusCode": 500, "error": "Test error"}) # additional_log_data self.assertTrue(call_kwargs.get("is_error_log", False)) # Should be error log @@ -215,12 +233,8 @@ def test_send_log_to_firehose_exception_logging(self): # Verify firehose_client.put_record was called expected_record = {"Data": json.dumps({"event": test_log_data}).encode("utf-8")} self.mock_firehose_client.put_record.assert_called_once_with( - DeliveryStreamName=self.test_stream, - Record=expected_record + DeliveryStreamName=self.test_stream, Record=expected_record ) # Verify logger.exception was called with the correct message and error - self.mock_logger_exception.assert_called_once_with( - "Error sending log to Firehose: %s", - test_error - ) + self.mock_logger_exception.assert_called_once_with("Error sending log to Firehose: %s", test_error) diff --git a/lambdas/shared/tests/test_common/test_pds_service.py b/lambdas/shared/tests/test_common/test_pds_service.py index fa5d14f93..0cdb11138 100644 --- a/lambdas/shared/tests/test_common/test_pds_service.py +++ b/lambdas/shared/tests/test_common/test_pds_service.py @@ -23,12 +23,15 @@ def test_get_patient_details(self): """it should send a GET request to PDS""" patient_id = "900000009" act_res = {"id": patient_id} - exp_header = { - 'Authorization': f'Bearer {self.access_token}' - } + exp_header = {"Authorization": f"Bearer {self.access_token}"} pds_url = f"{self.base_url}/{patient_id}" - responses.add(responses.GET, pds_url, json=act_res, status=200, - match=[matchers.header_matcher(exp_header)]) + responses.add( + responses.GET, + pds_url, + json=act_res, + status=200, + match=[matchers.header_matcher(exp_header)], + ) # When patient = self.pds_service.get_patient_details(patient_id) diff --git a/lambdas/shared/tests/test_common/test_redis_client.py b/lambdas/shared/tests/test_common/test_redis_client.py index b01befef3..d965541d2 100644 --- a/lambdas/shared/tests/test_common/test_redis_client.py +++ b/lambdas/shared/tests/test_common/test_redis_client.py @@ -14,7 +14,7 @@ def setUp(self): self.mock_getenv = self.getenv_patch.start() self.mock_getenv.side_effect = lambda key, default=None: { "REDIS_HOST": self.REDIS_HOST, - "REDIS_PORT": self.REDIS_PORT + "REDIS_PORT": self.REDIS_PORT, }.get(key, default) self.redis_patch = patch("redis.StrictRedis") @@ -32,15 +32,15 @@ def test_os_environ(self): self.assertEqual(redis_client.REDIS_PORT, self.REDIS_PORT) def test_redis_client(self): - ''' Test redis client is not initialized on import ''' + """Test redis client is not initialized on import""" importlib.reload(redis_client) self.mock_redis.assert_not_called() def test_redis_client_initialization(self): - ''' Test redis client is initialized exactly once even with multiple invocations''' + """Test redis client is initialized exactly once even with multiple invocations""" importlib.reload(redis_client) redis_client.get_redis_client() redis_client.get_redis_client() self.mock_redis.assert_called_once_with(host=self.REDIS_HOST, port=self.REDIS_PORT, decode_responses=True) - self.assertTrue(hasattr(redis_client, 'redis_client')) + self.assertTrue(hasattr(redis_client, "redis_client")) self.assertIsInstance(redis_client.redis_client, self.mock_redis.return_value.__class__) diff --git a/lambdas/shared/tests/test_common/test_s3_event.py b/lambdas/shared/tests/test_common/test_s3_event.py index 562ef2817..302746513 100644 --- a/lambdas/shared/tests/test_common/test_s3_event.py +++ b/lambdas/shared/tests/test_common/test_s3_event.py @@ -13,42 +13,33 @@ def setUp(self): "awsRegion": "us-west-2", "eventTime": "1970-01-01T00:00:00.000Z", "eventName": "ObjectCreated:Put", - "userIdentity": { - "principalId": "my-example-user" - }, - "requestParameters": { - "sourceIPAddress": "172.16.0.1" - }, + "userIdentity": {"principalId": "my-example-user"}, + "requestParameters": {"sourceIPAddress": "172.16.0.1"}, "responseElements": { "x-amz-request-id": "C3D13FE58DE4C810", - "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" + "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD", }, "s3": { "s3SchemaVersion": "1.0", "configurationId": "my-test-config", "bucket": { - "name": "my-test-bucket", - "ownerIdentity": { - "principalId": "my-example-id" - }, - "arn": "arn:aws:s3:::my-test-bucket" + "name": "my-test-bucket", + "ownerIdentity": {"principalId": "my-example-id"}, + "arn": "arn:aws:s3:::my-test-bucket", }, "object": { - "key": "my-test-key.csv", - "size": 1024, - "eTag": "d41d8cd98f00b204e9800998ecf8427e", - "versionId": "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "sequencer": "0055AED6DCD90281E5" - } - } + "key": "my-test-key.csv", + "size": 1024, + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "versionId": "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "sequencer": "0055AED6DCD90281E5", + }, + }, } def test_s3_event(self): """Test initialization with S3 event""" - event = { - 'Records': [self.s3_record_dict], - 'eventSource': 'aws:s3' - } + event = {"Records": [self.s3_record_dict], "eventSource": "aws:s3"} s3_event = S3Event(event) @@ -63,12 +54,9 @@ def test_s3_event(self): def test_s3_event_with_multiple_records(self): """Test initialization with multiple s3 records""" s3_record_2 = self.s3_record_dict.copy() - s3_record_2['s3']['bucket']['name'] = 'my-second-test-bucket' + s3_record_2["s3"]["bucket"]["name"] = "my-second-test-bucket" - event = { - 'Records': [self.s3_record_dict, s3_record_2], - 'eventSource': 'aws:s3' - } + event = {"Records": [self.s3_record_dict, s3_record_2], "eventSource": "aws:s3"} s3_event = S3Event(event) @@ -81,10 +69,7 @@ def test_s3_event_with_multiple_records(self): def test_s3_event_with_no_records(self): """Test initialization with no s3 records""" - event = { - 'Records': [], - 'eventSource': 'aws:s3' - } + event = {"Records": [], "eventSource": "aws:s3"} s3_event = S3Event(event) diff --git a/mesh_infra/README.md b/mesh_infra/README.md index d53d5a052..81dbeb7ca 100644 --- a/mesh_infra/README.md +++ b/mesh_infra/README.md @@ -1,16 +1,21 @@ -# Important -The Mesh module is not idempotent, which is why it is kept separate from the main infrastructure folder. Each time the module is applied, it triggers the recreation of various AWS resources related to the Mesh configuration. +# Important + +The Mesh module is not idempotent, which is why it is kept separate from the main infrastructure folder. Each time the module is applied, it triggers the recreation of various AWS resources related to the Mesh configuration. There is 1 mesh mailbox for int that currently resides in the dev AWS account. This should be moved to the new INT AWS account once it becomes active. ## Running terraform -The general procedures are: + +The general procedures are: + 1. Set up your environment by creating a .env file with the following values. Note: some values may require customisation based on your specific setup. + ```dotenv ENVIRONMENT=int or prod AWS_PROFILE=your-profile BUCKET_NAME=(find mesh bucket name in aws s3) TF_VAR_key=state ``` + 2. Run `make init` to initialize the Terraform project. 3. Run `make plan` to review the proposed infrastructure changes. -4. Once you're confident in the plan and understand its impact, execute `make apply` to apply the changes. \ No newline at end of file +4. Once you're confident in the plan and understand its impact, execute `make apply` to apply the changes. diff --git a/mesh_processor/src/converter.py b/mesh_processor/src/converter.py index ee391f4aa..a7e660793 100644 --- a/mesh_processor/src/converter.py +++ b/mesh_processor/src/converter.py @@ -11,24 +11,17 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() -s3_client = boto3.client('s3') +s3_client = boto3.client("s3") def parse_headers(headers_str: str) -> dict[str, str]: - headers = dict( - header_str.split(":", 1) - for header_str in headers_str.split("\r\n") - if ":" in header_str - ) + headers = dict(header_str.split(":", 1) for header_str in headers_str.split("\r\n") if ":" in header_str) return {k.strip(): v.strip() for k, v in headers.items()} def parse_header_value(header_value: str) -> tuple[str, dict[str, str]]: main_value, *params = header_value.split(";") - parsed_params = dict( - param.strip().split("=", 1) - for param in params - ) + parsed_params = dict(param.strip().split("=", 1) for param in params) parsed_params = {k: v.strip('"') for k, v in parsed_params.items()} return main_value, parsed_params @@ -41,7 +34,7 @@ def read_until_part_start(input_file: BinaryIO, boundary: bytes) -> None: def read_headers_bytes(input_file: BinaryIO) -> bytes: - headers_bytes = b'' + headers_bytes = b"" while line := input_file.readline(): if line == b"\r\n": return headers_bytes @@ -68,7 +61,7 @@ def stream_part_body(input_file: BinaryIO, boundary: bytes, output_file: BinaryI if previous_line is not None: if found_part_end: # The final \r\n is part of the encapsulation boundary, so should not be included - output_file.write(previous_line.rstrip(b'\r\n')) + output_file.write(previous_line.rstrip(b"\r\n")) return else: output_file.write(previous_line) @@ -83,21 +76,17 @@ def move_file(source_bucket: str, source_key: str, destination_bucket: str, dest Bucket=destination_bucket, Key=destination_key, ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, - ExpectedSourceBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT + ExpectedSourceBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, ) s3_client.delete_object( Bucket=source_bucket, Key=source_key, - ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT + ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, ) def transfer_multipart_content(bucket_name: str, file_key: str, boundary: bytes, filename: str) -> None: - with open( - f"s3://{bucket_name}/{file_key}", - "rb", - transport_params={"client": s3_client} - ) as input_file: + with open(f"s3://{bucket_name}/{file_key}", "rb", transport_params={"client": s3_client}) as input_file: read_until_part_start(input_file, boundary) headers = read_part_headers(input_file) @@ -112,16 +101,17 @@ def transfer_multipart_content(bucket_name: str, file_key: str, boundary: bytes, "wb", transport_params={ "client": s3_client, - "client_kwargs": { - "S3.Client.create_multipart_upload": { - "ContentType": content_type - } - } - } + "client_kwargs": {"S3.Client.create_multipart_upload": {"ContentType": content_type}}, + }, ) as output_file: stream_part_body(input_file, boundary, output_file) - move_file(DESTINATION_BUCKET_NAME, f"streaming/{filename}", DESTINATION_BUCKET_NAME, filename) + move_file( + DESTINATION_BUCKET_NAME, + f"streaming/{filename}", + DESTINATION_BUCKET_NAME, + filename, + ) def process_record(record: dict) -> None: @@ -132,9 +122,9 @@ def process_record(record: dict) -> None: response = s3_client.head_object( Bucket=bucket_name, Key=file_key, - ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT + ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, ) - content_type = response['ContentType'] + content_type = response["ContentType"] media_type, content_type_params = parse_header_value(content_type) filename = response["Metadata"].get("mex-filename") or file_key @@ -149,7 +139,7 @@ def process_record(record: dict) -> None: CopySource={"Bucket": bucket_name, "Key": file_key}, Key=filename, ExpectedBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, - ExpectedSourceBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT + ExpectedSourceBucketOwner=EXPECTED_BUCKET_OWNER_ACCOUNT, ) logger.info(f"Transfer complete for {file_key}") @@ -169,10 +159,8 @@ def lambda_handler(event: dict, _context: dict) -> dict: logger.exception("Failed to process record") success = False - return { - 'statusCode': 200, - 'body': 'Files converted and uploaded successfully!' - } if success else { - 'statusCode': 500, - 'body': 'Errors occurred during processing' - } + return ( + {"statusCode": 200, "body": "Files converted and uploaded successfully!"} + if success + else {"statusCode": 500, "body": "Errors occurred during processing"} + ) diff --git a/mesh_processor/tests/test_converter.py b/mesh_processor/tests/test_converter.py index 2f0e2f106..25af4e13b 100644 --- a/mesh_processor/tests/test_converter.py +++ b/mesh_processor/tests/test_converter.py @@ -12,28 +12,41 @@ def invoke_lambda(file_key: str): # Local import so that globals can be mocked from converter import lambda_handler + return lambda_handler( { "Records": [ { "s3": { "bucket": {"name": "source-bucket"}, - "object": {"key": file_key} + "object": {"key": file_key}, } } ] }, - {} + {}, ) @mock_aws -@patch.dict(os.environ, {"DESTINATION_BUCKET_NAME": "destination-bucket", "ACCOUNT_ID": MOCK_MOTO_ACCOUNT_ID}) +@patch.dict( + os.environ, + { + "DESTINATION_BUCKET_NAME": "destination-bucket", + "ACCOUNT_ID": MOCK_MOTO_ACCOUNT_ID, + }, +) class TestLambdaHandler(TestCase): def setUp(self): s3 = boto3.client("s3", region_name="eu-west-2") - s3.create_bucket(Bucket="source-bucket", CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}) - s3.create_bucket(Bucket="destination-bucket", CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}) + s3.create_bucket( + Bucket="source-bucket", + CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}, + ) + s3.create_bucket( + Bucket="destination-bucket", + CreateBucketConfiguration={"LocationConstraint": "eu-west-2"}, + ) def test_non_multipart_content_type(self): s3 = boto3.client("s3", region_name="eu-west-2") @@ -44,7 +57,7 @@ def test_non_multipart_content_type(self): ContentType="text/csv", Metadata={ "mex-filename": "overridden-filename.csv", - } + }, ) result = invoke_lambda("test-csv-file.csv") @@ -81,59 +94,67 @@ def test_multipart_content_type(self): cases = [ ( "standard", - "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "Content-Type: text/csv", - "", - "some CSV content", - "--12345678--", - "" - ]) + "\r\n".join( + [ + "", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "Content-Type: text/csv", + "", + "some CSV content", + "--12345678--", + "", + ] + ), ), ( "missing initial newline", - "\r\n".join([ - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "Content-Type: text/csv", - "", - "some CSV content", - "--12345678--", - "" - ]) + "\r\n".join( + [ + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "Content-Type: text/csv", + "", + "some CSV content", + "--12345678--", + "", + ] + ), ), ( "missing final newline", - "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "Content-Type: text/csv", - "", - "some CSV content", - "--12345678--", - ]) + "\r\n".join( + [ + "", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "Content-Type: text/csv", + "", + "some CSV content", + "--12345678--", + ] + ), ), ( "multiple parts", - "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "Content-Type: text/csv", - "", - "some CSV content", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-ignored-file"', - "Content-Type: text/plain", - "", - "some ignored content", - "--12345678--", - "" - ]) - ) + "\r\n".join( + [ + "", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "Content-Type: text/csv", + "", + "some CSV content", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-ignored-file"', + "Content-Type: text/plain", + "", + "some ignored content", + "--12345678--", + "", + ] + ), + ), ] for msg, body in cases: with self.subTest(msg=msg, body=body): @@ -158,28 +179,23 @@ def test_multipart_content_type_without_filename_in_headers(self): cases = [ ( "no filename in header", - "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data', - "Content-Type: text/csv", - "", - "some CSV content", - "--12345678--", - "" - ]) + "\r\n".join( + [ + "", + "--12345678", + "Content-Disposition: form-data", + "Content-Type: text/csv", + "", + "some CSV content", + "--12345678--", + "", + ] + ), ), ( "no header", - "\r\n".join([ - "", - "--12345678", - "", - "some CSV content", - "--12345678--", - "" - ]) - ) + "\r\n".join(["", "--12345678", "", "some CSV content", "--12345678--", ""]), + ), ] for msg, body in cases: with self.subTest(msg=msg, body=body): @@ -199,15 +215,17 @@ def test_multipart_content_type_without_filename_in_headers(self): assert body == "some CSV content" def test_multipart_content_type_without_content_type_in_headers(self): - body = "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "", - "some CSV content", - "--12345678--", - "" - ]) + body = "\r\n".join( + [ + "", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "", + "some CSV content", + "--12345678--", + "", + ] + ) s3 = boto3.client("s3", region_name="eu-west-2") s3.put_object( Bucket="source-bucket", @@ -226,16 +244,18 @@ def test_multipart_content_type_without_content_type_in_headers(self): assert content_type == "application/octet-stream" def test_multipart_content_type_with_unix_line_endings(self): - body = "\r\n".join([ - "", - "--12345678", - 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', - "Content-Type: text/csv", - "", - "some CSV content\nsplit across\nmultiple lines", - "--12345678--", - "" - ]) + body = "\r\n".join( + [ + "", + "--12345678", + 'Content-Disposition: form-data; name="File"; filename="test-csv-file.csv"', + "Content-Type: text/csv", + "", + "some CSV content\nsplit across\nmultiple lines", + "--12345678--", + "", + ] + ) s3 = boto3.client("s3", region_name="eu-west-2") s3.put_object( Bucket="source-bucket", diff --git a/package-lock.json b/package-lock.json index 1cf94fee9..ced278083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "license": "MIT", "devDependencies": { "@redocly/cli": "^2.3.1", - "license-checker": "^25.0.1" + "husky": "^9.1.7", + "license-checker": "^25.0.1", + "lint-staged": "^16.2.3", + "prettier": "^3.6.2" } }, "node_modules/@babel/code-frame": { @@ -801,6 +804,22 @@ } } }, + "node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -987,12 +1006,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1074,6 +1094,85 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1131,6 +1230,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1311,6 +1420,19 @@ "dev": true, "license": "MIT" }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1453,10 +1575,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1544,6 +1667,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1616,10 +1752,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1773,6 +1910,22 @@ } } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1849,6 +2002,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2198,6 +2352,259 @@ "semver": "bin/semver" } }, + "node_modules/lint-staged": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.3.tgz", + "integrity": "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.4", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.3", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/listr2": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -2265,6 +2672,20 @@ "node": ">= 0.4" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2288,6 +2709,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", @@ -2404,6 +2838,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/nano-spawn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", + "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2616,6 +3063,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openapi-sampler": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.1.tgz", @@ -2745,6 +3208,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -2804,6 +3280,22 @@ "dev": true, "license": "MIT" }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -3166,6 +3658,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3366,6 +3882,52 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", @@ -3479,6 +4041,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3643,6 +4215,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -3912,10 +4485,11 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, diff --git a/package.json b/package.json index a5c1903b9..af6a1e5c4 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,26 @@ "scripts": { "lint": "redocly lint --skip-rule=security-defined specification/immunisation-fhir-api.yaml", "publish": "redocly bundle specification/immunisation-fhir-api.yaml --remove-unused-components --ext json -o build/immunisation-fhir-api.json", - "check-licenses": "node_modules/.bin/license-checker --failOn GPL --failOn LGPL" + "check-licenses": "node_modules/.bin/license-checker --failOn GPL --failOn LGPL", + "prepare": "husky" }, "author": "NHS Digital", "license": "MIT", "homepage": "https://github.com/NHSDigital/immunisation-fhir-api", "devDependencies": { + "@redocly/cli": "^2.3.1", + "husky": "^9.1.7", "license-checker": "^25.0.1", - "@redocly/cli": "^2.3.1" + "lint-staged": "^16.2.3", + "prettier": "^3.6.2" + }, + "lint-staged": { + "*": "prettier --ignore-unknown --write", + "*.py": [ + "poetry -P quality_checks run flake8", + "poetry -P quality_checks run black -l 121" + ], + "*.tf": "terraform fmt", + "immunisation-fhir-api.{yaml,json}": "redocly lint --skip-rule=security-defined" } } diff --git a/quality_checks/.flake8 b/quality_checks/.flake8 new file mode 100644 index 000000000..7c61ead7c --- /dev/null +++ b/quality_checks/.flake8 @@ -0,0 +1,16 @@ +[flake8] +max-line-length = 121 + +# TODO - add flake8-bugbear and switch to the following config? +# extend-select = B950 +# extend-ignore = E203,E501,E701 +extend-ignore = E203,E701 + +exclude = + .git, + __pycache__, + dist, + .venv, + node_modules, + .terraform, + tests, # TODO - we really should be linting tests as well but they're full of line too long errors diff --git a/quality_checks/Makefile b/quality_checks/Makefile new file mode 100644 index 000000000..8f144bddd --- /dev/null +++ b/quality_checks/Makefile @@ -0,0 +1,8 @@ +lint: + poetry run flake8 .. + +format: + poetry run black -l 121 .. + +format-check: + poetry run black -l 121 --check .. diff --git a/quality_checks/poetry.lock b/quality_checks/poetry.lock new file mode 100644 index 000000000..43185f0e3 --- /dev/null +++ b/quality_checks/poetry.lock @@ -0,0 +1,201 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "black" +version = "25.9.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, + {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, + {file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"}, + {file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"}, + {file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"}, + {file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"}, + {file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"}, + {file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"}, + {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, + {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, + {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, + {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, + {file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"}, + {file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"}, + {file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"}, + {file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"}, + {file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"}, + {file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"}, + {file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"}, + {file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"}, + {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, + {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.1.10" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pytokens" +version = "0.1.10" +description = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, + {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11" +content-hash = "70c45a4fa3975c3574bbf014931571fa196ed9f09d3da9bce3fbe086d6db048f" diff --git a/quality_checks/pyproject.toml b/quality_checks/pyproject.toml new file mode 100644 index 000000000..ce53a3710 --- /dev/null +++ b/quality_checks/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "quality-checks" +version = "0.1.0" +description = "dependencies required for code quality checks (linting, formatting etc.)" +authors = [ + {name = "Matt Jarvis",email = "matt.jarvis2@nhs.net"} +] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "flake8 (>=7.3.0,<8.0.0)", + "black (>=25.9.0,<26.0.0)", +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/recordprocessor/src/audit_table.py b/recordprocessor/src/audit_table.py index af25e9141..b31c72254 100644 --- a/recordprocessor/src/audit_table.py +++ b/recordprocessor/src/audit_table.py @@ -1,4 +1,5 @@ """Add the filename to the audit table and check for duplicates.""" + from typing import Optional from clients import dynamodb_client, logger @@ -6,12 +7,7 @@ from constants import AUDIT_TABLE_NAME, AuditTableKeys -def update_audit_table_status( - file_key: str, - message_id: str, - status: str, - error_details: Optional[str] = None -) -> None: +def update_audit_table_status(file_key: str, message_id: str, status: str, error_details: Optional[str] = None) -> None: """Updates the status in the audit table to the requested value""" update_expression = f"SET #{AuditTableKeys.STATUS} = :status" expression_attr_names = {f"#{AuditTableKeys.STATUS}": "status"} @@ -37,7 +33,7 @@ def update_audit_table_status( "The status of %s file, with message id %s, was successfully updated to %s in the audit table", file_key, message_id, - status + status, ) except Exception as error: # pylint: disable = broad-exception-caught diff --git a/recordprocessor/src/batch_processor.py b/recordprocessor/src/batch_processor.py index 129313b72..fccd9d0bc 100644 --- a/recordprocessor/src/batch_processor.py +++ b/recordprocessor/src/batch_processor.py @@ -6,14 +6,19 @@ from csv import DictReader from json import JSONDecodeError -from constants import FileStatus, FileNotProcessedReason, SOURCE_BUCKET_NAME, ARCHIVE_DIR_NAME, PROCESSING_DIR_NAME +from constants import ( + FileStatus, + FileNotProcessedReason, + SOURCE_BUCKET_NAME, + ARCHIVE_DIR_NAME, + PROCESSING_DIR_NAME, +) from process_row import process_row from mappings import map_target_disease from audit_table import update_audit_table_status from send_to_kinesis import send_to_kinesis from clients import logger from file_level_validation import file_level_validation, file_is_empty, move_file -from errors import NoOperationPermissions, InvalidHeaders from utils_for_recordprocessor import get_csv_content_dict_reader from typing import Optional @@ -29,8 +34,8 @@ def process_csv_to_fhir(incoming_message_body: dict) -> int: try: incoming_message_body["encoder"] = encoder interim_message_body = file_level_validation(incoming_message_body=incoming_message_body) - except (InvalidHeaders, NoOperationPermissions, Exception) as e: # pylint: disable=broad-exception-caught - logger.error(f"File level validation failed: {e}") # If the file is invalid, processing should cease + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(f"File level validation failed: {e}") # If the file is invalid, processing should cease return 0 file_id = interim_message_body.get("message_id") @@ -43,12 +48,20 @@ def process_csv_to_fhir(incoming_message_body: dict) -> int: target_disease = map_target_disease(vaccine) - row_count, err = process_rows(file_id, vaccine, supplier, file_key, allowed_operations, - created_at_formatted_string, csv_reader, target_disease) + row_count, err = process_rows( + file_id, + vaccine, + supplier, + file_key, + allowed_operations, + created_at_formatted_string, + csv_reader, + target_disease, + ) if err: if isinstance(err, UnicodeDecodeError): - """ resolves encoding issue VED-754 """ + """resolves encoding issue VED-754""" logger.warning(f"Encoding Error: {err}.") new_encoder = "cp1252" logger.info(f"Encode error at row {row_count} with {encoder}. Switch to {new_encoder}") @@ -57,8 +70,17 @@ def process_csv_to_fhir(incoming_message_body: dict) -> int: # load alternative encoder csv_reader = get_csv_content_dict_reader(f"{PROCESSING_DIR_NAME}/{file_key}", encoder=encoder) # re-read the file and skip processed rows - row_count, err = process_rows(file_id, vaccine, supplier, file_key, allowed_operations, - created_at_formatted_string, csv_reader, target_disease, row_count) + row_count, err = process_rows( + file_id, + vaccine, + supplier, + file_key, + allowed_operations, + created_at_formatted_string, + csv_reader, + target_disease, + row_count, + ) else: logger.error(f"Row Processing error: {err}") raise err @@ -67,7 +89,11 @@ def process_csv_to_fhir(incoming_message_body: dict) -> int: if file_is_empty(row_count): logger.warning("File was empty: %s. Moving file to archive directory.", file_key) - move_file(SOURCE_BUCKET_NAME, f"{PROCESSING_DIR_NAME}/{file_key}", f"{ARCHIVE_DIR_NAME}/{file_key}") + move_file( + SOURCE_BUCKET_NAME, + f"{PROCESSING_DIR_NAME}/{file_key}", + f"{ARCHIVE_DIR_NAME}/{file_key}", + ) file_status = f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.EMPTY}" update_audit_table_status(file_key, file_id, file_status) @@ -84,7 +110,7 @@ def process_rows( created_at_formatted_string: str, csv_reader: DictReader, target_disease: list[dict], - total_rows_processed_count: int = 0 + total_rows_processed_count: int = 0, ) -> tuple[int, Optional[Exception]]: """ Processes each row in the csv_reader starting from start_row. @@ -100,7 +126,7 @@ def process_rows( # Log progress every 1000 rows and the first 10 rows after a restart if total_rows_processed_count % 1000 == 0: logger.info(f"Process: {total_rows_processed_count+1}") - if start_row > 0 and row_count <= start_row+10: + if start_row > 0 and row_count <= start_row + 10: logger.info(f"Restarted Process (log up to first 10): {total_rows_processed_count+1}") # Process the row to obtain the details needed for the message_body and ack file details_from_processing = process_row(target_disease, allowed_operations, row) diff --git a/recordprocessor/src/clients.py b/recordprocessor/src/clients.py index 5b439b914..4628d36e0 100644 --- a/recordprocessor/src/clients.py +++ b/recordprocessor/src/clients.py @@ -13,7 +13,9 @@ s3_client = boto3_client("s3", region_name=REGION_NAME) kinesis_client = boto3_client( - "kinesis", region_name=REGION_NAME, config=Config(retries={"max_attempts": 3, "mode": "standard"}) + "kinesis", + region_name=REGION_NAME, + config=Config(retries={"max_attempts": 3, "mode": "standard"}), ) sqs_client = boto3_client("sqs", region_name=REGION_NAME) firehose_client = boto3_client("firehose", region_name=REGION_NAME) diff --git a/recordprocessor/src/constants.py b/recordprocessor/src/constants.py index f6ce84419..ffd690af5 100644 --- a/recordprocessor/src/constants.py +++ b/recordprocessor/src/constants.py @@ -64,6 +64,7 @@ class FileStatus: class FileNotProcessedReason(StrEnum): """Reasons why a file was not processed""" + UNAUTHORISED = "Unauthorised" EMPTY = "Empty file" @@ -114,5 +115,5 @@ class Permission(StrEnum): permission_to_operation_map = { Permission.CREATE: Operation.CREATE, Permission.UPDATE: Operation.UPDATE, - Permission.DELETE: Operation.DELETE + Permission.DELETE: Operation.DELETE, } diff --git a/recordprocessor/src/convert_to_fhir_imms_resource.py b/recordprocessor/src/convert_to_fhir_imms_resource.py index 430cdfdd7..08ef17624 100644 --- a/recordprocessor/src/convert_to_fhir_imms_resource.py +++ b/recordprocessor/src/convert_to_fhir_imms_resource.py @@ -23,7 +23,11 @@ def _decorate_immunization(imms: dict, row: Dict[str, str]) -> None: Add.item(imms, "recorded", row.get("RECORDED_DATE"), Convert.date) - Add.list_of_dict(imms, "identifier", {"value": row.get("UNIQUE_ID"), "system": row.get("UNIQUE_ID_URI")}) + Add.list_of_dict( + imms, + "identifier", + {"value": row.get("UNIQUE_ID"), "system": row.get("UNIQUE_ID_URI")}, + ) def _decorate_patient(imms: dict, row: Dict[str, str]) -> None: @@ -51,7 +55,12 @@ def _decorate_patient(imms: dict, row: Dict[str, str]) -> None: Add.list_of_dict(patient, "address", {"postalCode": person_postcode}) - Add.custom_item(patient, "identifier", nhs_number, [{"system": Urls.NHS_NUMBER, "value": nhs_number}]) + Add.custom_item( + patient, + "identifier", + nhs_number, + [{"system": Urls.NHS_NUMBER, "value": nhs_number}], + ) # Add patient name if there is at least one non-empty patient name value if any(_is_not_empty(value) for value in [person_surname, person_forename]): @@ -71,9 +80,21 @@ def _decorate_vaccine(imms: dict, row: Dict[str, str]) -> None: vax_prod_system = Urls.SNOMED # vaccineCode is a mandatory FHIR field. If no values are supplied a default null flavour code of 'NAVU' is used. if not (vax_prod_code or vax_prod_term): - vax_prod_code, vax_prod_term, vax_prod_system = "NAVU", "Not available", Urls.NULL_FLAVOUR_CODES + vax_prod_code, vax_prod_term, vax_prod_system = ( + "NAVU", + "Not available", + Urls.NULL_FLAVOUR_CODES, + ) imms["vaccineCode"] = { - "coding": [Generate.dictionary({"system": vax_prod_system, "code": vax_prod_code, "display": vax_prod_term})] + "coding": [ + Generate.dictionary( + { + "system": vax_prod_system, + "code": vax_prod_code, + "display": vax_prod_term, + } + ) + ] } Add.dictionary(imms, "manufacturer", {"display": row.get("VACCINE_MANUFACTURER")}) @@ -107,9 +128,19 @@ def _decorate_vaccination(imms: dict, row: Dict[str, str]) -> None: Add.item(imms, "primarySource", row.get("PRIMARY_SOURCE"), Convert.boolean) - Add.snomed(imms, "site", row.get("SITE_OF_VACCINATION_CODE"), row.get("SITE_OF_VACCINATION_TERM")) + Add.snomed( + imms, + "site", + row.get("SITE_OF_VACCINATION_CODE"), + row.get("SITE_OF_VACCINATION_TERM"), + ) - Add.snomed(imms, "route", row.get("ROUTE_OF_VACCINATION_CODE"), row.get("ROUTE_OF_VACCINATION_TERM")) + Add.snomed( + imms, + "route", + row.get("ROUTE_OF_VACCINATION_CODE"), + row.get("ROUTE_OF_VACCINATION_TERM"), + ) dose_quantity_values = [ dose_amount := row.get("DOSE_AMOUNT"), @@ -123,12 +154,22 @@ def _decorate_vaccination(imms: dict, row: Dict[str, str]) -> None: **({"system": Urls.SNOMED} if _is_not_empty(dose_unit_code) else {}), "code": dose_unit_code, } - Add.custom_item(imms, "doseQuantity", dose_quantity_values, Generate.dictionary(dose_quantity_dict)) + Add.custom_item( + imms, + "doseQuantity", + dose_quantity_values, + Generate.dictionary(dose_quantity_dict), + ) # If DOSE_SEQUENCE is empty, default FHIR "doseNumberString" to "Dose sequence not recorded", # otherwise assume the sender's intention is to supply a positive integer if _is_not_empty(dose_sequence := row.get("DOSE_SEQUENCE")): - Add.item(imms["protocolApplied"][0], "doseNumberPositiveInt", dose_sequence, Convert.integer) + Add.item( + imms["protocolApplied"][0], + "doseNumberPositiveInt", + dose_sequence, + Convert.integer, + ) else: Add.item(imms["protocolApplied"][0], "doseNumberString", "Dose sequence not recorded") @@ -156,7 +197,11 @@ def _decorate_performer(imms: dict, row: Dict[str, str]) -> None: if any(_is_not_empty(value) for value in organization_values): organization = {"actor": {"type": "Organization"}} - Add.dictionary(organization["actor"], "identifier", {"system": site_code_type_uri, "value": site_code}) + Add.dictionary( + organization["actor"], + "identifier", + {"system": site_code_type_uri, "value": site_code}, + ) imms["performer"].append(organization) @@ -165,7 +210,10 @@ def _decorate_performer(imms: dict, row: Dict[str, str]) -> None: # Set up the practitioner internal_practitioner_id = "Practitioner1" - practitioner = {"resourceType": "Practitioner", "id": internal_practitioner_id} + practitioner = { + "resourceType": "Practitioner", + "id": internal_practitioner_id, + } imms["performer"].append({"actor": {"reference": f"#{internal_practitioner_id}"}}) # Add practitioner name if there is at least one non-empty practitioner name value @@ -173,7 +221,10 @@ def _decorate_performer(imms: dict, row: Dict[str, str]) -> None: practitioner["name"] = [{}] Add.item(practitioner["name"][0], "family", performing_prof_surname) Add.custom_item( - practitioner["name"][0], "given", [performing_prof_forename], [performing_prof_forename] + practitioner["name"][0], + "given", + [performing_prof_forename], + [performing_prof_forename], ) # Add practitioner to contained list if it exists, else create a contained list and add it to imms @@ -182,7 +233,10 @@ def _decorate_performer(imms: dict, row: Dict[str, str]) -> None: Add.custom_item( imms, "location", - [location_code := row.get("LOCATION_CODE"), location_code_type_uri := row.get("LOCATION_CODE_TYPE_URI")], + [ + location_code := row.get("LOCATION_CODE"), + location_code_type_uri := row.get("LOCATION_CODE_TYPE_URI"), + ], {"identifier": Generate.dictionary({"value": location_code, "system": location_code_type_uri})}, ) @@ -196,7 +250,9 @@ def _decorate_performer(imms: dict, row: Dict[str, str]) -> None: ] -def _get_decorators_for_action_flag(action_flag: Operation) -> List[ImmunizationDecorator]: +def _get_decorators_for_action_flag( + action_flag: Operation, +) -> List[ImmunizationDecorator]: # VED-32 DELETE action only requires the immunisation decorator if action_flag == Operation.DELETE: return [_decorate_immunization] @@ -210,7 +266,7 @@ def convert_to_fhir_imms_resource(row: dict, target_disease: list, action_flag: imms_resource = { "resourceType": "Immunization", "status": "completed", - "protocolApplied": [{"targetDisease": target_disease}] + "protocolApplied": [{"targetDisease": target_disease}], } required_decorators = _get_decorators_for_action_flag(action_flag) diff --git a/recordprocessor/src/file_level_validation.py b/recordprocessor/src/file_level_validation.py index 6541129f9..1bcf62359 100644 --- a/recordprocessor/src/file_level_validation.py +++ b/recordprocessor/src/file_level_validation.py @@ -2,6 +2,7 @@ Functions for completing file-level validation (validating headers and ensuring that the supplier has permission to perform at least one of the requested operations) """ + from csv import DictReader from clients import logger, s3_client @@ -10,8 +11,16 @@ from errors import InvalidHeaders, NoOperationPermissions from logging_decorator import file_level_validation_logging_decorator from audit_table import update_audit_table_status -from constants import SOURCE_BUCKET_NAME, EXPECTED_CSV_HEADERS, permission_to_operation_map, FileStatus, Permission, \ - FileNotProcessedReason, ARCHIVE_DIR_NAME, PROCESSING_DIR_NAME +from constants import ( + SOURCE_BUCKET_NAME, + EXPECTED_CSV_HEADERS, + permission_to_operation_map, + FileStatus, + Permission, + FileNotProcessedReason, + ARCHIVE_DIR_NAME, + PROCESSING_DIR_NAME, +) def validate_content_headers(csv_content_reader: DictReader) -> None: @@ -25,9 +34,7 @@ def file_is_empty(row_count: int) -> bool: return row_count == 0 -def get_permitted_operations( - supplier: str, vaccine_type: str, allowed_permissions_list: list -) -> set: +def get_permitted_operations(supplier: str, vaccine_type: str, allowed_permissions_list: list) -> set: # Check if supplier has permission for the subject vaccine type and extract permissions permission_strs_for_vaccine_type = { permission_str @@ -45,14 +52,11 @@ def get_permitted_operations( # Map Permission key to action flag permitted_operations_for_vaccine_type = { - permission_to_operation_map[permission].value - for permission in permissions_for_vaccine_type + permission_to_operation_map[permission].value for permission in permissions_for_vaccine_type } if not permitted_operations_for_vaccine_type: - raise NoOperationPermissions( - f"{supplier} does not have permissions to perform any of the requested actions." - ) + raise NoOperationPermissions(f"{supplier} does not have permissions to perform any of the requested actions.") return permitted_operations_for_vaccine_type @@ -121,8 +125,11 @@ def file_level_validation(incoming_message_body: dict) -> dict: file_key = file_key or "Unable to ascertain file_key" created_at_formatted_string = created_at_formatted_string or "Unable to ascertain created_at_formatted_string" make_and_upload_ack_file(message_id, file_key, False, False, created_at_formatted_string) - file_status = f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.UNAUTHORISED}"\ - if isinstance(error, NoOperationPermissions) else FileStatus.FAILED + file_status = ( + f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.UNAUTHORISED}" + if isinstance(error, NoOperationPermissions) + else FileStatus.FAILED + ) try: move_file(SOURCE_BUCKET_NAME, file_key, f"{ARCHIVE_DIR_NAME}/{file_key}") diff --git a/recordprocessor/src/logging_decorator.py b/recordprocessor/src/logging_decorator.py index 1a0ebd261..4901f4e52 100644 --- a/recordprocessor/src/logging_decorator.py +++ b/recordprocessor/src/logging_decorator.py @@ -22,11 +22,18 @@ def send_log_to_firehose(log_data: dict) -> None: def generate_and_send_logs( - start_time, base_log_data: dict, additional_log_data: dict, is_error_log: bool = False + start_time, + base_log_data: dict, + additional_log_data: dict, + is_error_log: bool = False, ) -> None: """Generates log data which includes the base_log_data, additional_log_data, and time taken (calculated using the current time and given start_time) and sends them to Cloudwatch and Firehose.""" - log_data = {**base_log_data, "time_taken": f"{round(time.time() - start_time, 5)}s", **additional_log_data} + log_data = { + **base_log_data, + "time_taken": f"{round(time.time() - start_time, 5)}s", + **additional_log_data, + } log_function = logger.error if is_error_log else logger.info log_function(json.dumps(log_data)) send_log_to_firehose(log_data) @@ -53,7 +60,10 @@ def wrapper(*args, **kwargs): try: result = func(*args, **kwargs) - additional_log_data = {"statusCode": 200, "message": "Successfully sent for record processing"} + additional_log_data = { + "statusCode": 200, + "message": "Successfully sent for record processing", + } generate_and_send_logs(start_time, base_log_data, additional_log_data=additional_log_data) return result @@ -61,10 +71,12 @@ def wrapper(*args, **kwargs): message = ( str(e) if (isinstance(e, InvalidHeaders) or isinstance(e, NoOperationPermissions)) else "Server error" ) - status_code = ( - 400 if isinstance(e, InvalidHeaders) else 403 if isinstance(e, NoOperationPermissions) else 500 - ) - additional_log_data = {"statusCode": status_code, "message": message, "error": str(e)} + status_code = 400 if isinstance(e, InvalidHeaders) else 403 if isinstance(e, NoOperationPermissions) else 500 + additional_log_data = { + "statusCode": status_code, + "message": message, + "error": str(e), + } generate_and_send_logs(start_time, base_log_data, additional_log_data, is_error_log=True) raise diff --git a/recordprocessor/src/make_and_upload_ack_file.py b/recordprocessor/src/make_and_upload_ack_file.py index b1f6c7d8f..d71e137d4 100644 --- a/recordprocessor/src/make_and_upload_ack_file.py +++ b/recordprocessor/src/make_and_upload_ack_file.py @@ -7,7 +7,10 @@ def make_ack_data( - message_id: str, validation_passed: bool, message_delivered: bool, created_at_formatted_string + message_id: str, + validation_passed: bool, + message_delivered: bool, + created_at_formatted_string, ) -> dict: """Returns a dictionary of ack data based on the input values. Dictionary keys are the ack file headers, dictionary values are the values for the ack file row""" @@ -46,7 +49,11 @@ def upload_ack_file(file_key: str, ack_data: dict, created_at_formatted_string: def make_and_upload_ack_file( - message_id: str, file_key: str, validation_passed: bool, message_delivered: bool, created_at_formatted_string: str + message_id: str, + file_key: str, + validation_passed: bool, + message_delivered: bool, + created_at_formatted_string: str, ) -> None: """Creates the ack file and uploads it to the S3 ack bucket""" ack_data = make_ack_data(message_id, validation_passed, message_delivered, created_at_formatted_string) diff --git a/recordprocessor/src/mappings.py b/recordprocessor/src/mappings.py index c2deaa451..c82e6ef4c 100644 --- a/recordprocessor/src/mappings.py +++ b/recordprocessor/src/mappings.py @@ -1,4 +1,5 @@ """Mappings for converting vaccine type into target disease FHIR element""" + import json from constants import Urls from clients import redis_client diff --git a/recordprocessor/src/process_row.py b/recordprocessor/src/process_row.py index cf8d66ee5..88dd65902 100644 --- a/recordprocessor/src/process_row.py +++ b/recordprocessor/src/process_row.py @@ -19,7 +19,10 @@ def process_row(target_disease: list, allowed_operations: set, row: dict) -> dic # Handle invalid action_flag if action_flag not in ("NEW", "UPDATE", "DELETE"): - logger.info("Invalid ACTION_FLAG '%s' - ACTION_FLAG MUST BE 'NEW', 'UPDATE' or 'DELETE'", action_flag) + logger.info( + "Invalid ACTION_FLAG '%s' - ACTION_FLAG MUST BE 'NEW', 'UPDATE' or 'DELETE'", + action_flag, + ) return { "diagnostics": create_diagnostics_dictionary("INVALID_ACTION_FLAG", 400, Diagnostics.INVALID_ACTION_FLAG), "operation_requested": action_flag, @@ -32,7 +35,10 @@ def process_row(target_disease: list, allowed_operations: set, row: dict) -> dic # Handle no permissions if operation_requested not in allowed_operations: - logger.info("Skipping row as supplier does not have the permissions for this operation %s", operation_requested) + logger.info( + "Skipping row as supplier does not have the permissions for this operation %s", + operation_requested, + ) return { "diagnostics": create_diagnostics_dictionary("NO_PERMISSIONS", 403, Diagnostics.NO_PERMISSIONS), "operation_requested": operation_requested, diff --git a/recordprocessor/src/utils_for_fhir_conversion.py b/recordprocessor/src/utils_for_fhir_conversion.py index 5f1c1a576..e25b4418d 100644 --- a/recordprocessor/src/utils_for_fhir_conversion.py +++ b/recordprocessor/src/utils_for_fhir_conversion.py @@ -133,7 +133,9 @@ def extension_item(url: str, system: str, code: str, display: str) -> dictionary extension_item = {"url": url, "valueCodeableConcept": {}} Add.list_of_dict( - extension_item["valueCodeableConcept"], "coding", {"system": system, "code": code, "display": display} + extension_item["valueCodeableConcept"], + "coding", + {"system": system, "code": code, "display": display}, ) return extension_item diff --git a/recordprocessor/src/utils_for_recordprocessor.py b/recordprocessor/src/utils_for_recordprocessor.py index 9ea10851a..1c874d010 100644 --- a/recordprocessor/src/utils_for_recordprocessor.py +++ b/recordprocessor/src/utils_for_recordprocessor.py @@ -23,4 +23,8 @@ def get_csv_content_dict_reader(file_key: str, encoder="utf-8") -> DictReader: def create_diagnostics_dictionary(error_type, status_code, error_message) -> dict: """Returns a dictionary containing the error_type, statusCode, and error_message""" - return {"error_type": error_type, "statusCode": status_code, "error_message": error_message} + return { + "error_type": error_type, + "statusCode": status_code, + "error_message": error_message, + } diff --git a/recordprocessor/tests/test_audit_table.py b/recordprocessor/tests/test_audit_table.py index 8d3478f9c..91fd79c44 100644 --- a/recordprocessor/tests/test_audit_table.py +++ b/recordprocessor/tests/test_audit_table.py @@ -1,4 +1,5 @@ """Tests for audit_table functions""" + import unittest from unittest import TestCase from unittest.mock import patch @@ -6,9 +7,17 @@ from moto import mock_dynamodb from errors import UnhandledAuditTableError -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT -from tests.utils_for_recordprocessor_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFileDetails, FileDetails +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) +from tests.utils_for_recordprocessor_tests.generic_setup_and_teardown import ( + GenericSetUp, + GenericTearDown, +) +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFileDetails, + FileDetails, +) from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( add_entry_to_table, ) @@ -55,7 +64,10 @@ def test_update_audit_table_status(self): add_entry_to_table(MockFileDetails.rsv_ravs, file_status=FileStatus.PROCESSING) add_entry_to_table(MockFileDetails.flu_emis, file_status=FileStatus.QUEUED) - expected_table_entry = {**MockFileDetails.rsv_ravs.audit_table_entry, "status": {"S": FileStatus.PREPROCESSED}} + expected_table_entry = { + **MockFileDetails.rsv_ravs.audit_table_entry, + "status": {"S": FileStatus.PREPROCESSED}, + } ravs_rsv_test_file = FileDetails("RSV", "RAVS", "X26") file_key = ravs_rsv_test_file.file_key message_id = ravs_rsv_test_file.message_id @@ -70,16 +82,23 @@ def test_update_audit_table_status_including_error_details(self): add_entry_to_table(MockFileDetails.rsv_ravs, file_status=FileStatus.QUEUED) ravs_rsv_test_file = FileDetails("RSV", "RAVS", "X26") - update_audit_table_status(ravs_rsv_test_file.file_key, ravs_rsv_test_file.message_id, FileStatus.FAILED, - error_details="Test error details") + update_audit_table_status( + ravs_rsv_test_file.file_key, + ravs_rsv_test_file.message_id, + FileStatus.FAILED, + error_details="Test error details", + ) table_items = dynamodb_client.scan(TableName=AUDIT_TABLE_NAME).get("Items", []) self.assertEqual(1, len(table_items)) - self.assertEqual({ - **MockFileDetails.rsv_ravs.audit_table_entry, - "status": {"S": FileStatus.FAILED}, - "error_details": {"S": "Test error details"} - }, table_items[0]) + self.assertEqual( + { + **MockFileDetails.rsv_ravs.audit_table_entry, + "status": {"S": FileStatus.FAILED}, + "error_details": {"S": "Test error details"}, + }, + table_items[0], + ) def test_update_audit_table_status_throws_exception_with_invalid_id(self): emis_flu_test_file_2 = FileDetails("FLU", "EMIS", "YGM41") diff --git a/recordprocessor/tests/test_convert_to_fhir_imms_decorators.py b/recordprocessor/tests/test_convert_to_fhir_imms_decorators.py index fb9539bff..a3f12fbb3 100644 --- a/recordprocessor/tests/test_convert_to_fhir_imms_decorators.py +++ b/recordprocessor/tests/test_convert_to_fhir_imms_decorators.py @@ -16,8 +16,12 @@ ExtensionItems, RSV_TARGET_DISEASE_ELEMENT, ) -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFieldDictionaries -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFieldDictionaries, +) +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from constants import Urls @@ -119,7 +123,13 @@ def test_no_vaccine_headers(self): "status": "completed", "protocolApplied": [{"targetDisease": RSV_TARGET_DISEASE_ELEMENT}], "vaccineCode": { - "coding": [{"system": Urls.NULL_FLAVOUR_CODES, "code": "NAVU", "display": "Not available"}] + "coding": [ + { + "system": Urls.NULL_FLAVOUR_CODES, + "code": "NAVU", + "display": "Not available", + } + ] }, }, ) @@ -157,7 +167,10 @@ def test_no_vaccination_headers(self): "resourceType": "Immunization", "status": "completed", "protocolApplied": [ - {"targetDisease": RSV_TARGET_DISEASE_ELEMENT, "doseNumberString": "Dose sequence not recorded"} + { + "targetDisease": RSV_TARGET_DISEASE_ELEMENT, + "doseNumberString": "Dose sequence not recorded", + } ], } self.assertDictEqual(self.imms, expected_output) @@ -202,9 +215,18 @@ def test_route_of_vaccination(self): def test_dose_quantity(self): """Test that only non-empty dose_quantity values (dose_amount, dose_unit_term and dose_unit_code) are added""" - dose_quantity = {"system": Urls.SNOMED, "value": Decimal("0.5"), "unit": "t", "code": "code"} + dose_quantity = { + "system": Urls.SNOMED, + "value": Decimal("0.5"), + "unit": "t", + "code": "code", + } # dose: _amount non-empty, _unit_term non-empty, _unit_code empty - headers = {"DOSE_AMOUNT": "0.5", "DOSE_UNIT_TERM": "a_dose_unit_term", "DOSE_UNIT_CODE": ""} + headers = { + "DOSE_AMOUNT": "0.5", + "DOSE_UNIT_TERM": "a_dose_unit_term", + "DOSE_UNIT_CODE": "", + } _decorate_vaccination(self.imms, headers) dose_quantity = {"value": Decimal("0.5"), "unit": "a_dose_unit_term"} self.assertDictEqual(self.imms["doseQuantity"], dose_quantity) @@ -212,13 +234,24 @@ def test_dose_quantity(self): # dose: _amount non-empty, _unit_term empty, _unit_code non-empty headers = {"DOSE_AMOUNT": "0.5", "DOSE_UNIT_CODE": "a_dose_unit_code"} _decorate_vaccination(self.imms, headers) - dose_quantity = {"system": Urls.SNOMED, "value": Decimal("0.5"), "code": "a_dose_unit_code"} + dose_quantity = { + "system": Urls.SNOMED, + "value": Decimal("0.5"), + "code": "a_dose_unit_code", + } self.assertDictEqual(self.imms["doseQuantity"], dose_quantity) # dose: _amount empty, _unit_term non-empty, _unit_code non-empty - headers = {"DOSE_UNIT_TERM": "a_dose_unit_term", "DOSE_UNIT_CODE": "a_dose_unit_code"} + headers = { + "DOSE_UNIT_TERM": "a_dose_unit_term", + "DOSE_UNIT_CODE": "a_dose_unit_code", + } _decorate_vaccination(self.imms, headers) - dose_quantity = {"system": Urls.SNOMED, "code": "a_dose_unit_code", "unit": "a_dose_unit_term"} + dose_quantity = { + "system": Urls.SNOMED, + "code": "a_dose_unit_code", + "unit": "a_dose_unit_term", + } self.assertDictEqual(self.imms["doseQuantity"], dose_quantity) # dose: _amount non-empty, _unit_term empty, _unit_code empty @@ -263,7 +296,14 @@ def test_one_performer_header(self): "resourceType": "Immunization", "status": "completed", "protocolApplied": [{"targetDisease": RSV_TARGET_DISEASE_ELEMENT}], - "performer": [{"actor": {"type": "Organization", "identifier": {"value": "a_site_code"}}}], + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": {"value": "a_site_code"}, + } + } + ], } self.assertDictEqual(self.imms, expected_output) diff --git a/recordprocessor/tests/test_convert_to_fhir_imms_resource.py b/recordprocessor/tests/test_convert_to_fhir_imms_resource.py index 5643bf5f2..27dd97921 100644 --- a/recordprocessor/tests/test_convert_to_fhir_imms_resource.py +++ b/recordprocessor/tests/test_convert_to_fhir_imms_resource.py @@ -1,4 +1,5 @@ """Tests for convert_to_fhir_imms_resource""" + import unittest from typing import Tuple, List from unittest.mock import patch @@ -6,9 +7,11 @@ from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( MockFhirImmsResources, MockFieldDictionaries, - TargetDiseaseElements + TargetDiseaseElements, +) +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT with patch("os.environ", MOCK_ENVIRONMENT_DICT): from convert_to_fhir_imms_resource import ( @@ -16,7 +19,7 @@ _get_decorators_for_action_flag, all_decorators, convert_to_fhir_imms_resource, - ImmunizationDecorator + ImmunizationDecorator, ) @@ -31,25 +34,30 @@ def test_convert_to_fhir_imms_resource(self): # Test cases tuples are structured as (test_name, input_values, expected_output, action_flag) test_cases = [ - ("All fields", MockFieldDictionaries.all_fields, MockFhirImmsResources.all_fields, "UPDATE"), + ( + "All fields", + MockFieldDictionaries.all_fields, + MockFhirImmsResources.all_fields, + "UPDATE", + ), ( "Mandatory fields only", MockFieldDictionaries.mandatory_fields_only, MockFhirImmsResources.mandatory_fields_only, - "UPDATE" + "UPDATE", ), ( "Critical fields only", MockFieldDictionaries.critical_fields_only, MockFhirImmsResources.critical_fields, - "NEW" + "NEW", ), ( "Delete action only converts minimal fields", MockFieldDictionaries.mandatory_fields_delete_action, MockFhirImmsResources.delete_operation_fields, - "DELETE" - ) + "DELETE", + ), ] for test_name, input_values, expected_output, action_flag in test_cases: @@ -63,9 +71,13 @@ def test_get_decorators_for_action_flag(self): action flag provided. """ test_cases: List[Tuple[str, str, List[ImmunizationDecorator]]] = [ - ("Delete action only returns one decorator", "DELETE", [_decorate_immunization]), + ( + "Delete action only returns one decorator", + "DELETE", + [_decorate_immunization], + ), ("Update action returns all decorators", "UPDATE", all_decorators), - ("Create action returns all decorators", "CREATE", all_decorators) + ("Create action returns all decorators", "CREATE", all_decorators), ] for test_name, action_flag, expected_decorators in test_cases: diff --git a/recordprocessor/tests/test_file_level_validation.py b/recordprocessor/tests/test_file_level_validation.py index 4e1ac1147..b94b6aa07 100644 --- a/recordprocessor/tests/test_file_level_validation.py +++ b/recordprocessor/tests/test_file_level_validation.py @@ -5,9 +5,16 @@ # If mock_s3 is not imported here then tests in other files fail when running 'make test'. It is not clear why this is. from moto import mock_s3 # noqa: F401 -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import convert_string_to_dict_reader -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFileDetails, ValidMockFileContent -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + convert_string_to_dict_reader, +) +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFileDetails, + ValidMockFileContent, +) +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from errors import NoOperationPermissions, InvalidHeaders diff --git a/recordprocessor/tests/test_logging_decorator.py b/recordprocessor/tests/test_logging_decorator.py index 2ea74557b..c8359b230 100644 --- a/recordprocessor/tests/test_logging_decorator.py +++ b/recordprocessor/tests/test_logging_decorator.py @@ -9,7 +9,10 @@ from boto3 import client as boto3_client from moto import mock_s3, mock_firehose -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFileDetails, ValidMockFileContent +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFileDetails, + ValidMockFileContent, +) from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( MOCK_ENVIRONMENT_DICT, BucketNames, @@ -23,7 +26,10 @@ from file_level_validation import file_level_validation -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import GenericSetUp, GenericTearDown +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + GenericSetUp, + GenericTearDown, +) s3_client = boto3_client("s3", region_name=REGION_NAME) firehose_client = boto3_client("firehose", region_name=REGION_NAME) @@ -103,7 +109,11 @@ def test_generate_and_send_logs(self): mock_time.time.return_value = 1672531200.123456 # Mocks the end time to be 0.123456s after the start time generate_and_send_logs(start_time, base_log_data, additional_log_data, is_error_log=False) - expected_log_data = {"base_key": "base_value", "time_taken": "0.12346s", "additional_key": "additional_value"} + expected_log_data = { + "base_key": "base_value", + "time_taken": "0.12346s", + "additional_key": "additional_value", + } log_data = json.loads(mock_logger.info.call_args[0][0]) self.assertEqual(log_data, expected_log_data) mock_send_log_to_firehose.assert_called_once_with(expected_log_data) @@ -117,7 +127,11 @@ def test_generate_and_send_logs(self): mock_time.time.return_value = 1672531200.123456 # Mocks the end time to be 0.123456s after the start time generate_and_send_logs(start_time, base_log_data, additional_log_data, is_error_log=True) - expected_log_data = {"base_key": "base_value", "time_taken": "0.12346s", "additional_key": "additional_value"} + expected_log_data = { + "base_key": "base_value", + "time_taken": "0.12346s", + "additional_key": "additional_value", + } log_data = json.loads(mock_logger.error.call_args[0][0]) self.assertEqual(log_data, expected_log_data) mock_send_log_to_firehose.assert_called_once_with(expected_log_data) @@ -142,7 +156,11 @@ def test_splunk_logger_successful_validation(self): file_level_validation(deepcopy(MOCK_FILE_DETAILS.event_full_permissions_dict)) expected_message = "Successfully sent for record processing" - expected_log_data = {**COMMON_LOG_DATA, "statusCode": 200, "message": expected_message} + expected_log_data = { + **COMMON_LOG_DATA, + "statusCode": 200, + "message": expected_message, + } # Log data is the first positional argument of the first call to logger.info log_data = json.loads(mock_logger.info.call_args_list[0][0][0]) @@ -186,7 +204,11 @@ def test_splunk_logger_handled_failure(self): ) in test_cases: with self.subTest(expected_error_message): - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=MOCK_FILE_DETAILS.file_key, Body=mock_file_content) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=MOCK_FILE_DETAILS.file_key, + Body=mock_file_content, + ) with ( # noqa: E999 patch("logging_decorator.datetime") as mock_datetime, # noqa: E999 @@ -212,7 +234,8 @@ def test_splunk_logger_handled_failure(self): expected_firehose_record = {"Data": json.dumps({"event": log_data}).encode("utf-8")} mock_firehose_client.put_record.assert_called_once_with( - DeliveryStreamName=Firehose.STREAM_NAME, Record=expected_firehose_record + DeliveryStreamName=Firehose.STREAM_NAME, + Record=expected_firehose_record, ) def test_splunk_logger_unhandled_failure(self): @@ -229,12 +252,13 @@ def test_splunk_logger_unhandled_failure(self): patch("logging_decorator.logger") as mock_logger, # noqa: E999 patch("logging_decorator.firehose_client") as mock_firehose_client, # noqa: E999 patch( - "file_level_validation.validate_content_headers", side_effect=Exception("Test exception") + "file_level_validation.validate_content_headers", + side_effect=ValueError("Test exception"), ), # noqa: E999 ): # noqa: E999 mock_time.time.side_effect = [1672531200, 1672531200.123456] mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0) - with self.assertRaises(Exception): + with self.assertRaises(ValueError): file_level_validation(deepcopy(MOCK_FILE_DETAILS.event_full_permissions_dict)) expected_log_data = { diff --git a/recordprocessor/tests/test_make_and_upload_ack_file.py b/recordprocessor/tests/test_make_and_upload_ack_file.py index 167563a4f..60bf80139 100644 --- a/recordprocessor/tests/test_make_and_upload_ack_file.py +++ b/recordprocessor/tests/test_make_and_upload_ack_file.py @@ -5,15 +5,29 @@ from copy import deepcopy from boto3 import client as boto3_client from moto import mock_s3 -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import get_csv_file_dict_reader -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFileDetails +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, +) +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + get_csv_file_dict_reader, +) +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFileDetails, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): - from make_and_upload_ack_file import make_ack_data, upload_ack_file, make_and_upload_ack_file + from make_and_upload_ack_file import ( + make_ack_data, + upload_ack_file, + make_and_upload_ack_file, + ) from clients import REGION_NAME -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import GenericSetUp, GenericTearDown +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + GenericSetUp, + GenericTearDown, +) s3_client = boto3_client("s3", region_name=REGION_NAME) @@ -90,7 +104,10 @@ def test_make_ack_data(self): with self.subTest(): self.assertEqual( make_ack_data( - self.message_id, validation_passed, message_delivered, self.created_at_formatted_string + self.message_id, + validation_passed, + message_delivered, + self.created_at_formatted_string, ), expected_result, ) diff --git a/recordprocessor/tests/test_map_target_disease.py b/recordprocessor/tests/test_map_target_disease.py index 337525e4c..5ec0b2aa4 100644 --- a/recordprocessor/tests/test_map_target_disease.py +++ b/recordprocessor/tests/test_map_target_disease.py @@ -1,8 +1,11 @@ """Tests for map_target_disease""" + import json import unittest from unittest.mock import patch -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from mappings import map_target_disease @@ -17,18 +20,29 @@ class TestMapTargetDisease(unittest.TestCase): def test_map_target_disease_valid(self, mock_redis_client): """Tests map_target_disease returns the disease coding information when using valid vaccine types""" - mock_redis_client.hget.return_value = json.dumps([{ - "code": "55735004", - "term": "Respiratory syncytial virus infection (disorder)" - }]) - - self.assertEqual(map_target_disease("RSV"), [{ - "coding": [{ - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - }] - }]) + mock_redis_client.hget.return_value = json.dumps( + [ + { + "code": "55735004", + "term": "Respiratory syncytial virus infection (disorder)", + } + ] + ) + + self.assertEqual( + map_target_disease("RSV"), + [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)", + } + ] + } + ], + ) mock_redis_client.hget.assert_called_with("vacc_to_diseases", "RSV") diff --git a/recordprocessor/tests/test_process_csv_to_fhir.py b/recordprocessor/tests/test_process_csv_to_fhir.py index 748ee1bc5..3ab097e67 100644 --- a/recordprocessor/tests/test_process_csv_to_fhir.py +++ b/recordprocessor/tests/test_process_csv_to_fhir.py @@ -1,4 +1,5 @@ """Tests for process_csv_to_fhir function""" + import json import unittest from unittest.mock import patch @@ -10,13 +11,18 @@ GenericSetUp, GenericTearDown, ) -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import add_entry_to_table +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + add_entry_to_table, +) from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( MockFileDetails, ValidMockFileContent, REGION_NAME, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from constants import FileStatus, AUDIT_TABLE_NAME @@ -36,18 +42,30 @@ class TestProcessCsvToFhir(unittest.TestCase): """Tests for process_csv_to_fhir function""" def setUp(self) -> None: - GenericSetUp(s3_client=s3_client, firehose_client=firehose_client, dynamodb_client=dynamodb_client) + GenericSetUp( + s3_client=s3_client, + firehose_client=firehose_client, + dynamodb_client=dynamodb_client, + ) redis_patcher = patch("mappings.redis_client") self.addCleanup(redis_patcher.stop) mock_redis_client = redis_patcher.start() - mock_redis_client.hget.return_value = json.dumps([{ - "code": "55735004", - "term": "Respiratory syncytial virus infection (disorder)" - }]) + mock_redis_client.hget.return_value = json.dumps( + [ + { + "code": "55735004", + "term": "Respiratory syncytial virus infection (disorder)", + } + ] + ) def tearDown(self) -> None: - GenericTearDown(s3_client=s3_client, firehose_client=firehose_client, dynamodb_client=dynamodb_client) + GenericTearDown( + s3_client=s3_client, + firehose_client=firehose_client, + dynamodb_client=dynamodb_client, + ) @staticmethod def upload_source_file(file_key, file_content): @@ -61,10 +79,14 @@ def test_process_csv_to_fhir_full_permissions(self): Tests that process_csv_to_fhir sends a message to kinesis for each row in the csv when the supplier has full permissions """ - expected_table_entry = {**test_file.audit_table_entry, "status": {"S": FileStatus.PREPROCESSED}} + expected_table_entry = { + **test_file.audit_table_entry, + "status": {"S": FileStatus.PREPROCESSED}, + } add_entry_to_table(test_file, FileStatus.PROCESSING) self.upload_source_file( - file_key=test_file.file_key, file_content=ValidMockFileContent.with_new_and_update_and_delete + file_key=test_file.file_key, + file_content=ValidMockFileContent.with_new_and_update_and_delete, ) with patch("batch_processor.send_to_kinesis") as mock_send_to_kinesis: @@ -80,10 +102,14 @@ def test_process_csv_to_fhir_partial_permissions(self): Tests that process_csv_to_fhir sends a message to kinesis for each row in the csv when the supplier has partial permissions """ - expected_table_entry = {**test_file.audit_table_entry, "status": {"S": FileStatus.PREPROCESSED}} + expected_table_entry = { + **test_file.audit_table_entry, + "status": {"S": FileStatus.PREPROCESSED}, + } add_entry_to_table(test_file, FileStatus.PROCESSING) self.upload_source_file( - file_key=test_file.file_key, file_content=ValidMockFileContent.with_new_and_update_and_delete + file_key=test_file.file_key, + file_content=ValidMockFileContent.with_new_and_update_and_delete, ) with patch("batch_processor.send_to_kinesis") as mock_send_to_kinesis: @@ -96,15 +122,25 @@ def test_process_csv_to_fhir_partial_permissions(self): def test_process_csv_to_fhir_no_permissions(self): """Tests that process_csv_to_fhir does not send fhir_json to kinesis when the supplier has no permissions""" - expected_table_entry = {**test_file.audit_table_entry, "status": {"S": FileStatus.PREPROCESSED}} + expected_table_entry = { + **test_file.audit_table_entry, + "status": {"S": FileStatus.PREPROCESSED}, + } add_entry_to_table(test_file, FileStatus.PROCESSING) - self.upload_source_file(file_key=test_file.file_key, file_content=ValidMockFileContent.with_update_and_delete) + self.upload_source_file( + file_key=test_file.file_key, + file_content=ValidMockFileContent.with_update_and_delete, + ) with patch("batch_processor.send_to_kinesis") as mock_send_to_kinesis: process_csv_to_fhir(deepcopy(test_file.event_create_permissions_only_dict)) self.assertEqual(mock_send_to_kinesis.call_count, 2) - for (_supplier, message_body, _vaccine), _kwargs in mock_send_to_kinesis.call_args_list: + for ( + _supplier, + message_body, + _vaccine, + ), _kwargs in mock_send_to_kinesis.call_args_list: self.assertIn("diagnostics", message_body) self.assertNotIn("fhir_json", message_body) diff --git a/recordprocessor/tests/test_process_row.py b/recordprocessor/tests/test_process_row.py index 8aa6e6f56..d730a2fe3 100644 --- a/recordprocessor/tests/test_process_row.py +++ b/recordprocessor/tests/test_process_row.py @@ -10,14 +10,16 @@ from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( MockFieldDictionaries, - TargetDiseaseElements + TargetDiseaseElements, ) from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( GenericSetUp, GenericTearDown, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from clients import REGION_NAME @@ -48,16 +50,22 @@ def test_process_row_success(self): expected_result = { "resourceType": "Immunization", "status": "completed", - "protocolApplied": [{ - "targetDisease": [{ - "coding": [{ - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - }] - }], - "doseNumberPositiveInt": 1 - }], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)", + } + ] + } + ], + "doseNumberPositiveInt": 1, + } + ], "reasonCode": [{"coding": [{"system": "http://snomed.info/sct", "code": "1037351000000105"}]}], "recorded": "2024-09-04", "identifier": [{"value": "RSV_002", "system": "https://www.ravs.england.nhs.uk/"}], @@ -69,7 +77,12 @@ def test_process_row_success(self): "birthDate": "2008-02-17", "gender": "male", "address": [{"postalCode": "WD25 0DZ"}], - "identifier": [{"system": "https://fhir.nhs.uk/Id/nhs-number", "value": "9732928395"}], + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9732928395", + } + ], "name": [{"family": "PEEL", "given": ["PHYLIS"]}], }, { @@ -106,9 +119,23 @@ def test_process_row_success(self): ], "occurrenceDateTime": "2024-09-04T18:33:25+00:00", "primarySource": True, - "site": {"coding": [{"system": "http://snomed.info/sct", "code": "368209003", "display": "Right arm"}]}, + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368209003", + "display": "Right arm", + } + ] + }, "route": { - "coding": [{"system": "http://snomed.info/sct", "code": "1210999013", "display": "Intradermal use"}] + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1210999013", + "display": "Intradermal use", + } + ] }, "doseQuantity": { "value": Decimal("0.3"), @@ -120,12 +147,20 @@ def test_process_row_success(self): { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "RVVKC"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RVVKC", + }, } }, {"actor": {"reference": "#Practitioner1"}}, ], - "location": {"identifier": {"value": "RJC02", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}}, + "location": { + "identifier": { + "value": "RJC02", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + } + }, } self.maxDiff = None @@ -205,7 +240,10 @@ def test_process_row_missing_unique_id_uri(self): # call 'process_row' with required details response = process_row(TargetDiseaseElements.RSV, Allowed_Operations, Mock_Row) - self.assertEqual(response["diagnostics"]["error_message"], "UNIQUE_ID or UNIQUE_ID_URI is missing") + self.assertEqual( + response["diagnostics"]["error_message"], + "UNIQUE_ID or UNIQUE_ID_URI is missing", + ) self.assertEqual(response["diagnostics"]["statusCode"], 400) diff --git a/recordprocessor/tests/test_recordprocessor_edge_cases.py b/recordprocessor/tests/test_recordprocessor_edge_cases.py index 4221b0695..20140e233 100644 --- a/recordprocessor/tests/test_recordprocessor_edge_cases.py +++ b/recordprocessor/tests/test_recordprocessor_edge_cases.py @@ -3,7 +3,9 @@ from io import BytesIO from unittest.mock import call, patch from batch_processor import process_csv_to_fhir -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import create_patch +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + create_patch, +) class TestProcessorEdgeCases(unittest.TestCase): @@ -33,13 +35,11 @@ def expand_test_data(self, data: list[bytes], num_rows: int) -> list[bytes]: header = data[0:1] body = data[1:] * multiplier data = header + body - data = data[:num_rows + 1] + data = data[: num_rows + 1] return data def create_test_data_from_file(self, file_name: str) -> list[bytes]: - test_csv_path = os.path.join( - os.path.dirname(__file__), "test_data", file_name - ) + test_csv_path = os.path.join(os.path.dirname(__file__), "test_data", file_name) with open(test_csv_path, "rb") as f: data = f.readlines() return data @@ -56,21 +56,21 @@ def insert_cp1252_at_end(self, data: list[bytes], new_text: bytes, field: int) - return data def test_process_large_file_cp1252(self): - """ Test processing a large file with cp1252 encoding """ + """Test processing a large file with cp1252 encoding""" n_rows = 500 data = self.create_test_data_from_file("test-batch-data.csv") data = self.expand_test_data(data, n_rows) - data = self.insert_cp1252_at_end(data, b'D\xe9cembre', 2) + data = self.insert_cp1252_at_end(data, b"D\xe9cembre", 2) ret1 = {"Body": BytesIO(b"".join(data))} ret2 = {"Body": BytesIO(b"".join(data))} self.mock_s3_get_object.side_effect = [ret1, ret2] self.mock_map_target_disease.return_value = "some disease" message_body = { - "vaccine_type": "vax-type-1", - "supplier": "test-supplier", - "filename": "test-filename" - } + "vaccine_type": "vax-type-1", + "supplier": "test-supplier", + "filename": "test-filename", + } self.mock_map_target_disease.return_value = "some disease" n_rows_processed = process_csv_to_fhir(message_body) @@ -80,13 +80,15 @@ def test_process_large_file_cp1252(self): self.mock_logger_warning.assert_called() warning_call_args = self.mock_logger_warning.call_args[0][0] self.assertTrue(warning_call_args.startswith("Encoding Error: 'utf-8' codec can't decode byte 0xe9")) - self.mock_s3_get_object.assert_has_calls([ - call(Bucket=None, Key="test-filename"), - call(Bucket=None, Key="processing/test-filename"), - ]) + self.mock_s3_get_object.assert_has_calls( + [ + call(Bucket=None, Key="test-filename"), + call(Bucket=None, Key="processing/test-filename"), + ] + ) def test_process_large_file_utf8(self): - """ Test processing a large file with utf-8 encoding """ + """Test processing a large file with utf-8 encoding""" n_rows = 500 data = self.create_test_data_from_file("test-batch-data.csv") data = self.expand_test_data(data, n_rows) @@ -96,9 +98,9 @@ def test_process_large_file_utf8(self): self.mock_map_target_disease.return_value = "some disease" message_body = { - "vaccine_type": "vax-type-1", - "supplier": "test-supplier", - } + "vaccine_type": "vax-type-1", + "supplier": "test-supplier", + } self.mock_map_target_disease.return_value = "some disease" n_rows_processed = process_csv_to_fhir(message_body) @@ -108,9 +110,9 @@ def test_process_large_file_utf8(self): self.mock_logger_error.assert_not_called() def test_process_small_file_cp1252(self): - """ Test processing a small file with cp1252 encoding """ + """Test processing a small file with cp1252 encoding""" data = self.create_test_data_from_file("test-batch-data-cp1252.csv") - data = self.insert_cp1252_at_end(data, b'D\xe9cembre', 2) + data = self.insert_cp1252_at_end(data, b"D\xe9cembre", 2) data = [line if line.endswith(b"\n") else line + b"\n" for line in data] n_rows = len(data) - 1 # Exclude header @@ -120,9 +122,9 @@ def test_process_small_file_cp1252(self): self.mock_map_target_disease.return_value = "some disease" message_body = { - "vaccine_type": "vax-type-1", - "supplier": "test-supplier", - } + "vaccine_type": "vax-type-1", + "supplier": "test-supplier", + } self.mock_map_target_disease.return_value = "some disease" @@ -134,7 +136,7 @@ def test_process_small_file_cp1252(self): self.assertTrue(warning_call_args.startswith("Invalid Encoding detected")) def test_process_small_file_utf8(self): - """ Test processing a small file with utf-8 encoding """ + """Test processing a small file with utf-8 encoding""" data = self.create_test_data_from_file("test-batch-data.csv") data = [line if line.endswith(b"\n") else line + b"\n" for line in data] n_rows = len(data) - 1 # Exclude header @@ -145,9 +147,9 @@ def test_process_small_file_utf8(self): self.mock_map_target_disease.return_value = "some disease" message_body = { - "vaccine_type": "vax-type-1", - "supplier": "test-supplier", - } + "vaccine_type": "vax-type-1", + "supplier": "test-supplier", + } self.mock_map_target_disease.return_value = "some disease" n_rows_processed = process_csv_to_fhir(message_body) diff --git a/recordprocessor/tests/test_recordprocessor_main.py b/recordprocessor/tests/test_recordprocessor_main.py index 1c95aaffe..a79e6247e 100644 --- a/recordprocessor/tests/test_recordprocessor_main.py +++ b/recordprocessor/tests/test_recordprocessor_main.py @@ -13,7 +13,7 @@ GenericSetUp, GenericTearDown, add_entry_to_table, - assert_audit_table_entry + assert_audit_table_entry, ) from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( MockFileDetails, @@ -25,11 +25,23 @@ InfAckFileRows, REGION_NAME, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, Kinesis -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import create_patch +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, + Kinesis, +) +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + create_patch, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): - from constants import Diagnostics, FileStatus, FileNotProcessedReason, AUDIT_TABLE_NAME, AuditTableKeys + from constants import ( + Diagnostics, + FileStatus, + FileNotProcessedReason, + AUDIT_TABLE_NAME, + AuditTableKeys, + ) from batch_processor import main s3_client = boto3_client("s3", region_name=REGION_NAME) @@ -56,10 +68,14 @@ def setUp(self) -> None: self.addCleanup(redis_patcher.stop) self.mock_batch_processor_logger = batch_processor_logger_patcher.start() mock_redis_client = redis_patcher.start() - mock_redis_client.hget.return_value = json.dumps([{ - "code": "55735004", - "term": "Respiratory syncytial virus infection (disorder)" - }]) + mock_redis_client.hget.return_value = json.dumps( + [ + { + "code": "55735004", + "term": "Respiratory syncytial virus infection (disorder)", + } + ] + ) self.mock_logger_info = create_patch("logging.Logger.info") def tearDown(self) -> None: @@ -67,9 +83,15 @@ def tearDown(self) -> None: GenericTearDown(s3_client, firehose_client, kinesis_client) @staticmethod - def upload_source_files(source_file_content): # pylint: disable=dangerous-default-value + def upload_source_files( + source_file_content, + ): # pylint: disable=dangerous-default-value """Uploads a test file with the TEST_FILE_KEY (RSV EMIS file) the given file content to the source bucket""" - s3_client.put_object(Bucket=BucketNames.SOURCE, Key=mock_rsv_emis_file.file_key, Body=source_file_content) + s3_client.put_object( + Bucket=BucketNames.SOURCE, + Key=mock_rsv_emis_file.file_key, + Body=source_file_content, + ) @staticmethod def get_shard_iterator(): @@ -134,7 +156,10 @@ def make_kinesis_assertions(self, test_cases): # Ensure that arrival times are sequential approximate_arrival_timestamp = kinesis_record["ApproximateArrivalTimestamp"] - self.assertGreater(approximate_arrival_timestamp, previous_approximate_arrival_time_stamp) + self.assertGreater( + approximate_arrival_timestamp, + previous_approximate_arrival_time_stamp, + ) previous_approximate_arrival_time_stamp = approximate_arrival_timestamp kinesis_data = json.loads(kinesis_record["Data"].decode("utf-8"), parse_float=Decimal) @@ -178,19 +203,28 @@ def test_e2e_full_permissions(self): ( "CREATE success", 0, - {"operation_requested": "CREATE", "local_id": MockLocalIds.RSV_001_RAVS}, + { + "operation_requested": "CREATE", + "local_id": MockLocalIds.RSV_001_RAVS, + }, True, ), ( "UPDATE success", 1, - {"operation_requested": "UPDATE", "local_id": MockLocalIds.COVID19_001_RAVS}, + { + "operation_requested": "UPDATE", + "local_id": MockLocalIds.COVID19_001_RAVS, + }, True, ), ( "DELETE success", 2, - {"operation_requested": "DELETE", "local_id": MockLocalIds.COVID19_001_RAVS}, + { + "operation_requested": "DELETE", + "local_id": MockLocalIds.COVID19_001_RAVS, + }, True, ), ] @@ -215,7 +249,10 @@ def test_e2e_partial_permissions(self): ( "CREATE success", 0, - {"operation_requested": "CREATE", "local_id": MockLocalIds.RSV_001_RAVS}, + { + "operation_requested": "CREATE", + "local_id": MockLocalIds.RSV_001_RAVS, + }, True, ), ( @@ -284,15 +321,19 @@ def test_e2e_no_permissions(self): kinesis_records = kinesis_client.get_records(ShardIterator=self.get_shard_iterator(), Limit=10)["Records"] table_entry = dynamo_db_client.get_item( - TableName=AUDIT_TABLE_NAME, Key={AuditTableKeys.MESSAGE_ID: {"S": test_file.message_id}} + TableName=AUDIT_TABLE_NAME, + Key={AuditTableKeys.MESSAGE_ID: {"S": test_file.message_id}}, ).get("Item") self.assertEqual(len(kinesis_records), 0) self.make_inf_ack_assertions(file_details=mock_rsv_emis_file, passed_validation=False) - self.assertDictEqual(table_entry, { - **test_file.audit_table_entry, - "status": {"S": f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.UNAUTHORISED}"}, - "error_details": {"S": "EMIS does not have permissions to perform any of the requested actions."} - }) + self.assertDictEqual( + table_entry, + { + **test_file.audit_table_entry, + "status": {"S": f"{FileStatus.NOT_PROCESSED} - {FileNotProcessedReason.UNAUTHORISED}"}, + "error_details": {"S": "EMIS does not have permissions to perform any of the requested actions."}, + }, + ) def test_e2e_invalid_action_flags(self): """Tests that file is successfully processed when the ACTION_FLAG field is empty or invalid.""" @@ -317,8 +358,18 @@ def test_e2e_invalid_action_flags(self): # Assertion case tuples are stuctured as # (test_name, index, expected_kinesis_data_ignoring_fhir_json,expect_success) assertion_cases = [ - ("Missing ACTION_FLAG", 0, {**expected_kinesis_data, "operation_requested": ""}, False), - ("Invalid ACTION_FLAG", 1, {**expected_kinesis_data, "operation_requested": "INVALID"}, False), + ( + "Missing ACTION_FLAG", + 0, + {**expected_kinesis_data, "operation_requested": ""}, + False, + ), + ( + "Invalid ACTION_FLAG", + 1, + {**expected_kinesis_data, "operation_requested": "INVALID"}, + False, + ), ] self.make_inf_ack_assertions(file_details=mock_rsv_emis_file, passed_validation=True) self.make_kinesis_assertions(assertion_cases) @@ -358,8 +409,18 @@ def test_e2e_differing_amounts_of_data(self): # Test case tuples are stuctured as (test_name, index, expected_kinesis_data, expect_success) test_cases = [ ("All fields", 0, all_fields_row_expected_kinesis_data, True), - ("Mandatory fields only", 1, mandatory_fields_only_row_expected_kinesis_data, True), - ("Critical fields only", 2, critical_fields_only_row_expected_kinesis_data, True), + ( + "Mandatory fields only", + 1, + mandatory_fields_only_row_expected_kinesis_data, + True, + ), + ( + "Critical fields only", + 2, + critical_fields_only_row_expected_kinesis_data, + True, + ), ] self.make_inf_ack_assertions(file_details=mock_rsv_emis_file, passed_validation=True) self.make_kinesis_assertions(test_cases) @@ -387,7 +448,8 @@ def test_e2e_kinesis_failed(self): # Since the failure occured at row level, not file level, the ack file should still be created # and firehose logs should indicate a successful file level validation table_entry = dynamo_db_client.get_item( - TableName=AUDIT_TABLE_NAME, Key={AuditTableKeys.MESSAGE_ID: {"S": test_file.message_id}} + TableName=AUDIT_TABLE_NAME, + Key={AuditTableKeys.MESSAGE_ID: {"S": test_file.message_id}}, ).get("Item") self.make_inf_ack_assertions(file_details=test_file, passed_validation=True) expected_log_data = { @@ -402,13 +464,18 @@ def test_e2e_kinesis_failed(self): "message": "Successfully sent for record processing", } mock_send_log_to_firehose.assert_called_with(expected_log_data) - self.assertDictEqual(table_entry, { - **test_file.audit_table_entry, - "status": {"S": FileStatus.FAILED}, - "error_details": {"S": "An error occurred (ResourceNotFoundException) when calling the PutRecord operation" - ": Stream imms-batch-internal-dev-processingdata-stream under account 123456789012" - " not found."} - }) + self.assertDictEqual( + table_entry, + { + **test_file.audit_table_entry, + "status": {"S": FileStatus.FAILED}, + "error_details": { + "S": "An error occurred (ResourceNotFoundException) when calling the PutRecord operation" + ": Stream imms-batch-internal-dev-processingdata-stream under account 123456789012" + " not found." + }, + }, + ) def test_e2e_empty_file_is_flagged_and_processed_correctly(self): """ @@ -416,8 +483,14 @@ def test_e2e_empty_file_is_flagged_and_processed_correctly(self): """ test_cases = [ ("File containing only headers", ValidMockFileContent.headers), - ("File containing headers and new line", ValidMockFileContent.headers + "\n"), - ("File containing headers and multiple new lines", ValidMockFileContent.empty_file_with_multiple_new_lines) + ( + "File containing headers and new line", + ValidMockFileContent.headers + "\n", + ), + ( + "File containing headers and multiple new lines", + ValidMockFileContent.empty_file_with_multiple_new_lines, + ), ] for description, file_content in test_cases: @@ -429,12 +502,13 @@ def test_e2e_empty_file_is_flagged_and_processed_correctly(self): main(test_file.event_full_permissions) - kinesis_records = kinesis_client.get_records( - ShardIterator=self.get_shard_iterator(), Limit=10)["Records"] + kinesis_records = kinesis_client.get_records(ShardIterator=self.get_shard_iterator(), Limit=10)[ + "Records" + ] self.mock_batch_processor_logger.warning.assert_called_once_with( "File was empty: %s. Moving file to archive directory.", - "RSV_Vaccinations_v5_8HK48_20210730T12000000.csv" + "RSV_Vaccinations_v5_8HK48_20210730T12000000.csv", ) self.assertListEqual(kinesis_records, []) assert_audit_table_entry(test_file, "Not processed - Empty file") @@ -443,7 +517,8 @@ def test_e2e_empty_file_is_flagged_and_processed_correctly(self): def test_e2e_error_is_logged_if_invalid_json_provided(self): """This scenario should not happen. If it does, it means our batch processing system config is broken and we have received malformed content from SQS -> EventBridge. In this case we log the error so we will be alerted. - However, we cannot do anything with the AuditDB record as we cannot retrieve information from the event""" + However, we cannot do anything with the AuditDB record as we cannot retrieve information from the event + """ malformed_event = '{"test": {}' main(malformed_event) diff --git a/recordprocessor/tests/test_send_to_kinesis.py b/recordprocessor/tests/test_send_to_kinesis.py index 5a3ce0265..d46e471f8 100644 --- a/recordprocessor/tests/test_send_to_kinesis.py +++ b/recordprocessor/tests/test_send_to_kinesis.py @@ -10,7 +10,9 @@ from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( REGION_NAME, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from send_to_kinesis import send_to_kinesis diff --git a/recordprocessor/tests/test_utils_for_fhir_conversion.py b/recordprocessor/tests/test_utils_for_fhir_conversion.py index c3979bac5..37431673d 100644 --- a/recordprocessor/tests/test_utils_for_fhir_conversion.py +++ b/recordprocessor/tests/test_utils_for_fhir_conversion.py @@ -3,7 +3,9 @@ import unittest from unittest.mock import patch from decimal import Decimal -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from constants import Urls @@ -60,7 +62,14 @@ def test_convert_date(self): self.assertEqual(Convert.date("19821115"), "1982-11-15") # Invalid_dates - for value in ["2000-01-01", 20000101, "20000230", "2000011", "990101", "20000101T00:00"]: + for value in [ + "2000-01-01", + 20000101, + "20000230", + "2000011", + "990101", + "20000101T00:00", + ]: self.assertEqual(Convert.date(value), value) def test_convert_gender_code(self): @@ -69,7 +78,12 @@ def test_convert_gender_code(self): the original value if this is not possible """ # Valid gender codes - for code, expected in [("1", "male"), ("2", "female"), ("9", "other"), ("0", "unknown")]: + for code, expected in [ + ("1", "male"), + ("2", "female"), + ("9", "other"), + ("0", "unknown"), + ]: self.assertEqual(Convert.gender_code(code), expected) # Invalid gender codes @@ -186,7 +200,12 @@ class TestBatchUtilsAdd(unittest.TestCase): """Tests for the batch utils Add functions""" def setUp(self): - self.test_dict_some_empty = {"key1": "value1", "key2": None, "key3": False, "key4": ""} + self.test_dict_some_empty = { + "key1": "value1", + "key2": None, + "key3": False, + "key4": "", + } self.test_dict_none_empty = {"key1": "value1", "key3": False} self.test_dict_empty = {"key5": "", "key6": None, "key7": []} diff --git a/recordprocessor/tests/test_utils_for_recordprocessor.py b/recordprocessor/tests/test_utils_for_recordprocessor.py index ed4288628..f7d31d8c4 100644 --- a/recordprocessor/tests/test_utils_for_recordprocessor.py +++ b/recordprocessor/tests/test_utils_for_recordprocessor.py @@ -6,13 +6,19 @@ import csv import boto3 from moto import mock_s3 -from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import GenericSetUp, GenericTearDown +from tests.utils_for_recordprocessor_tests.utils_for_recordprocessor_tests import ( + GenericSetUp, + GenericTearDown, +) from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( MockFileDetails, ValidMockFileContent, REGION_NAME, ) -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, + BucketNames, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from utils_for_recordprocessor import ( diff --git a/recordprocessor/tests/utils_for_recordprocessor_tests/decorator_constants.py b/recordprocessor/tests/utils_for_recordprocessor_tests/decorator_constants.py index 05a1a8fbb..0f15b43f5 100644 --- a/recordprocessor/tests/utils_for_recordprocessor_tests/decorator_constants.py +++ b/recordprocessor/tests/utils_for_recordprocessor_tests/decorator_constants.py @@ -2,8 +2,12 @@ from unittest.mock import patch from decimal import Decimal -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import TargetDiseaseElements -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + TargetDiseaseElements, +) +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from constants import Urls @@ -137,8 +141,24 @@ class AllHeadersExpectedOutput: "extension": [ExtensionItems.vaccination_procedure], "occurrenceDateTime": "2000-01-01T11:11:11+01:00", "primarySource": True, - "site": {"coding": [{"system": Urls.SNOMED, "code": "a_vacc_site_code", "display": "a_vacc_site_term"}]}, - "route": {"coding": [{"system": Urls.SNOMED, "code": "a_vacc_route_code", "display": "a_vacc_route_term"}]}, + "site": { + "coding": [ + { + "system": Urls.SNOMED, + "code": "a_vacc_site_code", + "display": "a_vacc_site_term", + } + ] + }, + "route": { + "coding": [ + { + "system": Urls.SNOMED, + "code": "a_vacc_route_code", + "display": "a_vacc_route_term", + } + ] + }, "doseQuantity": { "value": Decimal(0.5), "unit": "a_dose_unit_term", @@ -162,7 +182,10 @@ class AllHeadersExpectedOutput: { "actor": { "type": "Organization", - "identifier": {"system": "a_site_code_type_uri", "value": "a_site_code"}, + "identifier": { + "system": "a_site_code_type_uri", + "value": "a_site_code", + }, } }, {"actor": {"reference": "#Practitioner1"}}, diff --git a/recordprocessor/tests/utils_for_recordprocessor_tests/generic_setup_and_teardown.py b/recordprocessor/tests/utils_for_recordprocessor_tests/generic_setup_and_teardown.py index 742a8f6ee..cbd03a663 100644 --- a/recordprocessor/tests/utils_for_recordprocessor_tests/generic_setup_and_teardown.py +++ b/recordprocessor/tests/utils_for_recordprocessor_tests/generic_setup_and_teardown.py @@ -12,7 +12,12 @@ # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): from clients import REGION_NAME - from constants import AuditTableKeys, AUDIT_TABLE_QUEUE_NAME_GSI, AUDIT_TABLE_FILENAME_GSI, AUDIT_TABLE_NAME + from constants import ( + AuditTableKeys, + AUDIT_TABLE_QUEUE_NAME_GSI, + AUDIT_TABLE_FILENAME_GSI, + AUDIT_TABLE_NAME, + ) class GenericSetUp: @@ -25,7 +30,13 @@ class GenericSetUp: * If dynamodb_client is provided, creates the audit table """ - def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamodb_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + sqs_client=None, + dynamodb_client=None, + ): if s3_client: for bucket_name in [ BucketNames.SOURCE, @@ -33,7 +44,8 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo BucketNames.MOCK_FIREHOSE, ]: s3_client.create_bucket( - Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) if firehose_client: @@ -64,18 +76,35 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo GlobalSecondaryIndexes=[ { "IndexName": AUDIT_TABLE_FILENAME_GSI, - "KeySchema": [{"AttributeName": AuditTableKeys.FILENAME, "KeyType": "HASH"}], + "KeySchema": [ + { + "AttributeName": AuditTableKeys.FILENAME, + "KeyType": "HASH", + } + ], "Projection": {"ProjectionType": "KEYS_ONLY"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, { "IndexName": AUDIT_TABLE_QUEUE_NAME_GSI, "KeySchema": [ - {"AttributeName": AuditTableKeys.QUEUE_NAME, "KeyType": "HASH"}, - {"AttributeName": AuditTableKeys.STATUS, "KeyType": "RANGE"}, + { + "AttributeName": AuditTableKeys.QUEUE_NAME, + "KeyType": "HASH", + }, + { + "AttributeName": AuditTableKeys.STATUS, + "KeyType": "RANGE", + }, ], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, ], ) @@ -84,7 +113,13 @@ def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamo class GenericTearDown: """Performs generic tear down of mock resources""" - def __init__(self, s3_client=None, firehose_client=None, sqs_client=None, dynamodb_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + sqs_client=None, + dynamodb_client=None, + ): if s3_client: for bucket_name in [ diff --git a/recordprocessor/tests/utils_for_recordprocessor_tests/utils_for_recordprocessor_tests.py b/recordprocessor/tests/utils_for_recordprocessor_tests/utils_for_recordprocessor_tests.py index 24db4c669..d36a7c1e4 100644 --- a/recordprocessor/tests/utils_for_recordprocessor_tests/utils_for_recordprocessor_tests.py +++ b/recordprocessor/tests/utils_for_recordprocessor_tests/utils_for_recordprocessor_tests.py @@ -1,19 +1,33 @@ """Utils for the recordprocessor tests""" from io import StringIO -from tests.utils_for_recordprocessor_tests.mock_environment_variables import BucketNames, Firehose, Kinesis -from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import MockFileDetails, FileDetails +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + BucketNames, + Firehose, + Kinesis, +) +from tests.utils_for_recordprocessor_tests.values_for_recordprocessor_tests import ( + MockFileDetails, + FileDetails, +) from boto3.dynamodb.types import TypeDeserializer from boto3 import client as boto3_client from unittest.mock import patch -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) # Ensure environment variables are mocked before importing from src files with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT): from clients import REGION_NAME from csv import DictReader - from constants import AuditTableKeys, AUDIT_TABLE_NAME, FileStatus, AUDIT_TABLE_FILENAME_GSI, \ - AUDIT_TABLE_QUEUE_NAME_GSI + from constants import ( + AuditTableKeys, + AUDIT_TABLE_NAME, + FileStatus, + AUDIT_TABLE_FILENAME_GSI, + AUDIT_TABLE_QUEUE_NAME_GSI, + ) dynamodb_client = boto3_client("dynamodb", region_name=REGION_NAME) @@ -39,12 +53,23 @@ class GenericSetUp: * If kinesis_client is provided, creates a kinesis stream """ - def __init__(self, s3_client=None, firehose_client=None, kinesis_client=None, dynamo_db_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + kinesis_client=None, + dynamo_db_client=None, + ): if s3_client: - for bucket_name in [BucketNames.SOURCE, BucketNames.DESTINATION, BucketNames.MOCK_FIREHOSE]: + for bucket_name in [ + BucketNames.SOURCE, + BucketNames.DESTINATION, + BucketNames.MOCK_FIREHOSE, + ]: s3_client.create_bucket( - Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": REGION_NAME} + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": REGION_NAME}, ) if firehose_client: @@ -75,18 +100,35 @@ def __init__(self, s3_client=None, firehose_client=None, kinesis_client=None, dy GlobalSecondaryIndexes=[ { "IndexName": AUDIT_TABLE_FILENAME_GSI, - "KeySchema": [{"AttributeName": AuditTableKeys.FILENAME, "KeyType": "HASH"}], + "KeySchema": [ + { + "AttributeName": AuditTableKeys.FILENAME, + "KeyType": "HASH", + } + ], "Projection": {"ProjectionType": "KEYS_ONLY"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, { "IndexName": AUDIT_TABLE_QUEUE_NAME_GSI, "KeySchema": [ - {"AttributeName": AuditTableKeys.QUEUE_NAME, "KeyType": "HASH"}, - {"AttributeName": AuditTableKeys.STATUS, "KeyType": "RANGE"}, + { + "AttributeName": AuditTableKeys.QUEUE_NAME, + "KeyType": "HASH", + }, + { + "AttributeName": AuditTableKeys.STATUS, + "KeyType": "RANGE", + }, ], "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, }, ], ) @@ -95,7 +137,13 @@ def __init__(self, s3_client=None, firehose_client=None, kinesis_client=None, dy class GenericTearDown: """Performs generic tear down of mock resources""" - def __init__(self, s3_client=None, firehose_client=None, kinesis_client=None, dynamo_db_client=None): + def __init__( + self, + s3_client=None, + firehose_client=None, + kinesis_client=None, + dynamo_db_client=None, + ): if s3_client: for bucket_name in [BucketNames.SOURCE, BucketNames.DESTINATION]: @@ -133,9 +181,13 @@ def deserialize_dynamodb_types(dynamodb_table_entry_with_types): def assert_audit_table_entry(file_details: FileDetails, expected_status: FileStatus) -> None: """Assert that the file details are in the audit table""" table_entry = dynamodb_client.get_item( - TableName=AUDIT_TABLE_NAME, Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}} + TableName=AUDIT_TABLE_NAME, + Key={AuditTableKeys.MESSAGE_ID: {"S": file_details.message_id}}, ).get("Item") - assert table_entry == {**file_details.audit_table_entry, "status": {"S": expected_status}} + assert table_entry == { + **file_details.audit_table_entry, + "status": {"S": expected_status}, + } def create_patch(target: str): diff --git a/recordprocessor/tests/utils_for_recordprocessor_tests/values_for_recordprocessor_tests.py b/recordprocessor/tests/utils_for_recordprocessor_tests/values_for_recordprocessor_tests.py index 347d768c5..365014356 100644 --- a/recordprocessor/tests/utils_for_recordprocessor_tests/values_for_recordprocessor_tests.py +++ b/recordprocessor/tests/utils_for_recordprocessor_tests/values_for_recordprocessor_tests.py @@ -3,7 +3,9 @@ from unittest.mock import patch import json from decimal import Decimal -from tests.utils_for_recordprocessor_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT +from tests.utils_for_recordprocessor_tests.mock_environment_variables import ( + MOCK_ENVIRONMENT_DICT, +) with patch("os.environ", MOCK_ENVIRONMENT_DICT): from constants import Urls, AuditTableKeys @@ -167,10 +169,22 @@ def __init__(self, vaccine_type: str, supplier: str, ods_code: str, file_number: } # Mock the event details which would be receeived from SQS message - self.event_full_permissions_dict = {**self.base_event, "permission": self.full_permissions_list} - self.event_create_permissions_only_dict = {**self.base_event, "permission": self.create_permissions_only} - self.event_update_permissions_only_dict = {**self.base_event, "permission": self.update_permissions_only} - self.event_delete_permissions_only_dict = {**self.base_event, "permission": self.delete_permissions_only} + self.event_full_permissions_dict = { + **self.base_event, + "permission": self.full_permissions_list, + } + self.event_create_permissions_only_dict = { + **self.base_event, + "permission": self.create_permissions_only, + } + self.event_update_permissions_only_dict = { + **self.base_event, + "permission": self.update_permissions_only, + } + self.event_delete_permissions_only_dict = { + **self.base_event, + "permission": self.delete_permissions_only, + } self.event_no_permissions_dict = {**self.base_event, "permission": []} self.event_full_permissions = json.dumps(self.event_full_permissions_dict) self.event_create_permissions_only = json.dumps(self.event_create_permissions_only_dict) @@ -247,7 +261,11 @@ class UnorderedFieldDictionaries: "INDICATION_CODE": "1037351000000105", } - critical_fields = {"ACTION_FLAG": "NEW", "UNIQUE_ID": "a_unique_id", "UNIQUE_ID_URI": "a_unique_id_uri"} + critical_fields = { + "ACTION_FLAG": "NEW", + "UNIQUE_ID": "a_unique_id", + "UNIQUE_ID_URI": "a_unique_id_uri", + } class MockFieldDictionaries: @@ -365,11 +383,24 @@ class MockFhirImmsResources: "recorded": "2024-09-04", "primarySource": True, "manufacturer": {"display": "Sanofi Pasteur"}, - "location": {"identifier": {"value": "RJC02", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}}, + "location": { + "identifier": { + "value": "RJC02", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + } + }, "lotNumber": "BN92478105653", "expirationDate": "2024-09-15", "site": {"coding": [{"system": Urls.SNOMED, "code": "368209003", "display": "Right arm"}]}, - "route": {"coding": [{"system": Urls.SNOMED, "code": "1210999013", "display": "Intradermal use"}]}, + "route": { + "coding": [ + { + "system": Urls.SNOMED, + "code": "1210999013", + "display": "Intradermal use", + } + ] + }, "doseQuantity": { "value": Decimal("0.3"), "unit": "Inhalation - unit of product usage", @@ -380,7 +411,10 @@ class MockFhirImmsResources: { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "RVVKC"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RVVKC", + }, } }, {"actor": {"reference": "#Practitioner1"}}, @@ -426,17 +460,33 @@ class MockFhirImmsResources: } ], "status": "completed", - "vaccineCode": {"coding": [{"system": Urls.NULL_FLAVOUR_CODES, "code": "NAVU", "display": "Not available"}]}, + "vaccineCode": { + "coding": [ + { + "system": Urls.NULL_FLAVOUR_CODES, + "code": "NAVU", + "display": "Not available", + } + ] + }, "patient": {"reference": "#Patient1"}, "occurrenceDateTime": "2024-09-04T18:33:25+00:00", "recorded": "2024-09-04", "primarySource": True, - "location": {"identifier": {"value": "RJC02", "system": "https://fhir.nhs.uk/Id/ods-organization-code"}}, + "location": { + "identifier": { + "value": "RJC02", + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + } + }, "performer": [ { "actor": { "type": "Organization", - "identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": "RVVKC"}, + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RVVKC", + }, } }, ], @@ -451,7 +501,15 @@ class MockFhirImmsResources: critical_fields = { "resourceType": "Immunization", "status": "completed", - "vaccineCode": {"coding": [{"system": Urls.NULL_FLAVOUR_CODES, "code": "NAVU", "display": "Not available"}]}, + "vaccineCode": { + "coding": [ + { + "system": Urls.NULL_FLAVOUR_CODES, + "code": "NAVU", + "display": "Not available", + } + ] + }, "identifier": [{"system": "a_unique_id_uri", "value": "a_unique_id"}], "protocolApplied": [ { @@ -473,7 +531,7 @@ class MockFhirImmsResources: { "system": "http://snomed.info/sct", "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" + "display": "Respiratory syncytial virus infection (disorder)", } ] } @@ -481,12 +539,7 @@ class MockFhirImmsResources: } ], "recorded": "2024-09-04", - "identifier": [ - { - "value": "RSV_002", - "system": "https://www.ravs.england.nhs.uk/" - } - ] + "identifier": [{"value": "RSV_002", "system": "https://www.ravs.england.nhs.uk/"}], } diff --git a/sandbox/HealthStatusEndpoint.json b/sandbox/HealthStatusEndpoint.json index ffea57a3e..361000de5 100644 --- a/sandbox/HealthStatusEndpoint.json +++ b/sandbox/HealthStatusEndpoint.json @@ -1,22 +1,21 @@ { - "/_status": { - "get": { - "operationId": "healthcheck", - "summary": "healthcheck endpoint", - "responses": { - "200": { - "description": "Successful Operation", - "content": { - "application/text": { - "schema": { - "type": "string" - }, - "example": "OK" - } + "/_status": { + "get": { + "operationId": "healthcheck", + "summary": "healthcheck endpoint", + "responses": { + "200": { + "description": "Successful Operation", + "content": { + "application/text": { + "schema": { + "type": "string" + }, + "example": "OK" } } } } + } } - } - \ No newline at end of file +} diff --git a/sandbox/immunisation-fhir-api.json b/sandbox/immunisation-fhir-api.json index 2c4e08f5d..9ecacbd21 100644 --- a/sandbox/immunisation-fhir-api.json +++ b/sandbox/immunisation-fhir-api.json @@ -1,7418 +1,2925 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Immunization-fhir-api", - "version": "Computed and injected at build time by `scripts/set_version.py`", - "description": "## Overview\n \nUse this API to access a patient's immunisation record. It is part of the [Vaccinations Data Flow Management](https://digital.nhs.uk/services/vaccinations-data-flow-management). It is intended to extend and replace [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) and existing [Vaccination](https://digital.nhs.uk/developer/api-catalogue/vaccination) flows. \n\n### You can:\n \n- create and record a patient immunisation \n- search for a patient's immunisation records\n- get the details of an immunisation record\n- update an immunisation record \n- identify an immunisation record as entered in error \n \n### You cannot use this API to:\n \n- retrieve the immunisation record of multiple patients at once\n- record or update a patient's demographic details\n> To get demographic details from the [Personal Demographics Service](https://digital.nhs.uk/services/personal-demographics-service), use the Personal Demographics FHIR API.\n \nYou can create, read, update and delete:\n \n- Vaccination events for the disease types found in this list {To be confirm}\n \n### Data availability, timing and quality\n\nThis is a real time service, constrained by the time taken for providers to transfer vaccination events. In most cases the latest a record will be available is within 48 hours of the immunisation event.\n\nThe API search interaction will only return immunisation records based on a traced NHS number. Other interactions require the immunisation ID assigned by the API to interact with individual records for read, update and delete.\n\nThe vaccination events for all disease types are limited to vaccinations administered on behalf of NHS England.\n\nThere is a limited scope of data validation upon receipt of the data. Whilst the data is generally of a good, reliable quality, consumers must be aware data is shared as received and users should consider the risk of potential absences or inaccuracies of the data. \n\n \n## Who can use this API \nThis API can only be used where there is a commercial, legal and clinical basis to do so. Make sure you have a valid use case before you go too far with your development.\n\nYou must demonstrate you have a valid use case as part of digital onboarding. As this service is disease type agnostic, there is also a shortened onboarding process for each disease type, to explain the legal, commercial and clinical justifications. \n\nYou must do this before you can go live refer to the [Onboarding](https://digital.nhs.uk/developer/api-catalogue/immunisation-fhir-api#overview--onboarding).\n\n## Who can access immunisation event records\n \nHealth and care organisations in England can access immunisation records.\n\nLegitimate direct care examples include NHS organisations delivering healthcare, local authorities delivering care, third sector and private sector health and care organisations, and developers delivering systems to health and care organisations.\n\nA future capability will be to enable patients who receive health and social care or make use of NHS services in England to access their vaccination records.\n\nThe immunisation event records captured via the API are used for a number of purposes including (some uses include distribution of the data by means other than this API)\n \n- supporting a patient getting a vaccination\n- supporting patient managing their immunisation health\n- identifying those to be invited to vaccination nationally\n- identifying those to be invited to vaccination locally\n- supporting the booking of a vaccination appointment\n- supporting the payment reconciliation process on behalf of NHS England / NHS Business Services Authority\n- supporting measuring the overall success of a vaccination campaign to help inform the success of the [NHS Vaccination Strategy](https://www.england.nhs.uk/long-read/nhs-vaccination-strategy/)\n\n## API status and roadmap\nThis API is [in development](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#statuses).\n \nTo see our roadmap, or to suggest, comment or vote on features for this API, see our [interactive product backlog](https://nhs-digital-api-management.featureupvote.com/?tag=immunisation).\n \nIf you have any other queries, [contact us](https://digital.nhs.uk/developer/help-and-support).\n \n## Service level\nThis API will be a platinum service, meaning it is operational and supported 24 x 7 x 365.\n \nFor more details, see [service levels](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#service-levels).\n \n## Technology\n \nThis API is [RESTful](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#basic-rest).\n \nIt conforms to the [FHIR](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) global standard for health care data exchange, specifically to [FHIR R4 (v4.0.1)](https://hl7.org/fhir/r4/), except that it does not support the [capabilities](http://hl7.org/fhir/R4/http.html#capabilities) interaction.\n \nIt includes some country-specific FHIR extensions, which conform to [FHIR UK Core](https://digital.nhs.uk/services/fhir-uk-core), specifically [fhir.r4.ukcore.stu2](https://simplifier.net/packages/fhir.r4.ukcore.stu2).\n \nYou do not need to know much about FHIR to use this API - FHIR APIs are just RESTful APIs that follow specific rules.\nIn particular:\n- resource names are capitalised and singular, and use US spellings, for example `/Immunization` not `/immunisations`\n- array names are singular, for example `entry` not `entries` for address lines\n- data items that are country-specific and thus not included in the FHIR global base resources are usually wrapped in an `extension` object\n \nThere are [libraries and SDKs available](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) to help with FHIR API integration.\n \n## Network access\nThis API is available on the internet and, indirectly, on the [Health and Social Care Network (HSCN)](https://digital.nhs.uk/services/health-and-social-care-network).\n \nFor more details see [Network access for APIs](https://digital.nhs.uk/developer/guides-and-documentation/network-access-for-apis).\n \n## Security and authorisation\n \nThis API has a single access mode:\n- application-restricted access\n\nIn the future we intend to offer user-restricted access. \n \n### Application-restricted access\n \nThis access mode is [application-restricted](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation#application-restricted-apis), meaning we authenticate the calling application but not the end user.\n \nTo use this access mode, use the following security pattern:\n- [Application-restricted RESTful API - signed JWT authentication](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication)\n\nAccess may be restricted to certain functionality based on your user need\n \ncontact:\n \n name: Immunization-fhir-api API Support\n \n url: 'https://digital.nhs.uk/developer/help-and-support'\n \n email: api.management@nhs.net\n\n## Errors\nWe use standard HTTP status codes to show whether an API request succeeded or not. They are usually in the range:\n\n* 200 to 299 if it succeeded, including code 202 if it was accepted by an API that needs to wait for further action\n* 400 to 499 if it failed because of a client error by your application\n* 500 to 599 if it failed because of an error on our server\n\nErrors specific to each API are shown in the Endpoints section, under Response. See our [reference guide](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#http-status-codes) for more on errors.\n\n## Open source\n\nYou might find the following [open source](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#open-source) resources useful:\n\n| Resource | Description | Links |\n|---------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------|\n| Immunisation FHIR API | Source code for the API proxy, sandbox and specification. | [GitHub repo](https://github.dev/NHSDigital/immunisation-fhir-api/) |\n| FHIR libraries and SDKs | Various open source libraries for integrating with FHIR APIs. | [FHIR libraries and SDKs](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) |\n| nhs-number | Python package containing utilities for NHS numbers including validity checks, normalisation and generation. | [GitHub repo](https://github.com/uk-fci/nhs-number) \\| [Python Package index](https://pypi.org/project/nhs-number/) \\| [Docs](https://nhs-number.uk-fci.tech/) |\n\nWe currently don't have any open source client libraries or sample code for this API. If you think this would be useful, you can [upvote the suggestion on our Interactive Product Backlog](https://nhs-digital-api-management.featureupvote.com/suggestions/107439/client-libraries-and-reference-implementations).\n\nThe source code for the Immunisation FHIR API is not currently in the open. If you think this would be useful, you can [upvote the suggestion on our Interactive Product Backlog](https://nhs-digital-api-management.featureupvote.com/suggestions/466692/open-source-core-spine-including-pds-eps-scr-and-more).\n\n\n## Environments and Testing\n| Environment | Base URL |\n| ----------------- | --------------------------------------------------------------------- |\n| Sandbox | `https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n| Integration | `https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` | \n| Production | `https://api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n\n### Sandbox testing\nOur [sandbox environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#sandbox-testing):\n* is for early developer testing\n* only covers a limited set of scenarios\n* is stateless, so does not actually persist any updates\n* is open access, so does not allow you to test authorisation\n\nFor details of sandbox test scenarios, or to try out the sandbox using our 'Try this API' feature, see the documentation for each endpoint.\n\n### Integration testing\nOur [integration test environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing):\n* is for formal integration testing\n* is stateful, so persists updates\n* includes authorisation, with options for user-restricted access (with or without [smartcards](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/nhs-smartcards-for-developers)) and application-restricted access \n\nFor read-only testing, we will provide an Immunisation records test pack soon.\n\nTo test creating, updating and deleting patient vaccination events, you must set up your own test data.\n\nFor more details see [integration testing with our RESTful APIs](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing-with-our-restful-apis).\n\n## Onboarding\n \nYou need to get your software approved by us before it can go live with this API.\nWe call this onboarding.\nThe onboarding process can sometimes be quite long, so it is worth planning well ahead.\n\nWhilst this API is in Alpha it is not possible to onboard to this API.\nAs part of this process, you need to demonstrate that you can manage risks and that your software conforms technically with the requirements for this API.\nInformation on this page might impact the design of your software.\n\nOnboarding will initially be to a first of type for search and read for RSV vaccination records only. A phased rollout will then release further interactions and types of vaccination records.\n\nTo understand how our online digital onboarding process works, see [digital onboarding](https://digital.nhs.uk/developer/guides-and-documentation/digital-onboarding#using-the-digital-onboarding-portal).\n" - }, - "servers": [ - { - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", - "description": "Sandbox Server" - }, - { - "url": "https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", - "description": "Integration Server" - } - ], - "paths": { - "/Immunization": { - "post": { - "summary": "Record a vaccination given to a patient", - "operationId": "createImmunization", - "description": "## Overview\nUse this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the 'disease types' enabled in this interaction and represented by the correct SNOMED concept(s) for that 'disease type'. See another page for details and disease types and coding. \nYou must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n | Record a vaccination event | valid request as per schema | HTTP Status 201 with immunisation id in response header(location) |\n| Bad Request(missing/invalid required element in request body) | Didn't pass `resourceType` in request body | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - } - ], - "requestBody": { - "content": { - "application/fhir+json": { - "schema": { - "description": "A FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrenceDateTime", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "meta": { - "type": "object", - "description": "Metadata about the resource.", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - } - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "name": { - "type": "array", - "description": "Patient name as registered on PDS or as recorded by the user where the record cannot be traced on PDS. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "identifier", - "name", - "gender", - "birthDate", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - }, - "text": { - "description": "Plain text representation of the concept.", - "type": "string" - } - } - } - } - } - }, - "identifier": { - "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "A URI for the system that has allocated the vaccination identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" - }, - "value": { - "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrenceDateTime": { - "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \nOnly positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - } - }, - "manufacturer": { - "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", - "type": "object", - "properties": { - "display": { - "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - } - }, - "route": { - "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", - "type": "object", - "required": [], - "properties": { - "value": { - "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", - "type": "number", - "example": 1 - }, - "unit": { - "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", - "type": "object", - "properties": { - "type": { - "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", - "type": "string", - "example": "B0C4P" - } - } - }, - "reference": { - "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", - "type": "string", - "example": "#Pract1" - } - } - } - } - } - }, - "reasonCode": { - "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", - "type": "array", - "items": { - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. A code list will be provided for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - } - } - } - } - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. The use of an integer is preferred if known. A string should only be used in cases where an integer is not available.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available.", - "type": "integer", - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "a7437179-e86e-4855-b68e-24b5jhg3g" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07T13:28:17.271+00:00", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "responses": { - "201": { - "description": "Create Immunization operation successful", - "headers": { - "Location": { - "$ref": "#/components/parameters/Location" - }, - "CorrelationID": { - "$ref": "#/components/parameters/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/parameters/RequestID" - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms" - } - } - }, - "get": { - "summary": "Search (GET) for a patient's immunisation records", - "operationId": "searchViaGetImmunization", - "description": "## Overview\nUse this interaction to search for a patient's vaccination records using their NHS number and DiseaseType. You can request the patient's vaccination history for one or more specified 'disease types'. You may limit the vaccination records by specifying date criteria, for example if you only need to know about vaccinations administered in the last 12 months. \n Location related data items are included. Patient location sensitivity indicators (such as flags for sensitive patient records) should be obtained by connecting systems from the [Personal Demographics Service (PDS)](https://digital.nhs.uk/services/personal-demographics-service) and used to apply data filtering as appropriate. The response will not include contained resources for patient or practitioner within each immunization resource it returns. A single, separate patient resource will be included in the bundle and referenced by each immunization. \nVaccination events submitted without an NHS Number will not be available for retrieval via this interaction. Also, where a patient has a change of NHS Number some or all records may be unavailable via this interaction for a short period of time whilst records are updated. \nYou must be authorised for the search interaction and the disease type(s) specified in your search in order to access the records. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\\|9000000009` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `patient.identifier` or `-immunization.target` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/PatientIdentifier" - }, - { - "$ref": "#/components/parameters/ImmunizationTarget" - }, - { - "$ref": "#/components/parameters/DateFrom" - }, - { - "$ref": "#/components/parameters/DateTo" - }, - { - "$ref": "#/components/parameters/Include" - } - ], - "responses": { - "200": { - "description": "Search immunisation operation successful", - "content": { - "application/fhir+json": { - "schema": { - "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", - "type": "object", - "required": [ - "resourceType", - "type", - "total", - "entry" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Bundle`.", - "type": "string", - "example": "Bundle" - }, - "type": { - "description": "Indicates how the bundle is intended to be used. Always `searchset`.", - "type": "string", - "example": "searchset" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "relation": { - "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1). Always `Self`.", - "type": "string" - }, - "url": { - "description": "A url representing the search applied by the API to generate the result which may differ from the request if unrecognised or unsupported parameters have been ignored.", - "type": "string" - } - }, - "required": [ - "relation", - "url" - ] - } - }, - "entry": { - "description": "List of matching immunisations and associated patient. If there were no matching immunisations, this is an empty list.", - "type": "array", - "items": { - "type": "object", - "required": [ - "fullUrl", - "resource", - "search" - ], - "properties": { - "fullUrl": { - "description": "URI for the Immunization or Patient resource.", - "type": "string", - "example": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "resource": { - "description": "The Immunization or Patient resource.", - "oneOf": [ - { - "description": "A matching immunisation, formatted as a FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrenceDateTime", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "Identifies where the resource comes from." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - } - } - } - } - } - }, - "identifier": { - "description": "Unique identifier for this immunisation record, as generated by the source system.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "URI of the namespace of this identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc" - }, - "value": { - "description": "Identifier value within `system`.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who was immunised.", - "type": "object", - "required": [ - "reference", - "type", - "identifier" - ], - "properties": { - "reference": { - "description": "URI for the associated Patient resource in the bundle.", - "type": "string", - "example": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" - }, - "type": { - "description": "Type of resource this reference refers to. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "identifier": { - "description": "Business identifier for linked Patient. Always an NHS number.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "URI of coding system used to identify linked patient. Always https://fhir.nhs.uk/Id/nhs-number", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Value in coding system representing linked patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - }, - "occurrenceDateTime": { - "description": "Date and time of immunisation.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "required": [ - "identifier" - ] - }, - "manufacturer": { - "description": "Vaccine manufacturer details.", - "type": "object", - "properties": { - "display": { - "description": "Decsription of the vaccine manufacturer.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Lot number of the vaccine product.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Date vaccine batch expires.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - }, - "required": [ - "coding" - ] - }, - "route": { - "description": "The path by which the vaccine product is taken into the body.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered.", - "type": "object", - "required": [], - "properties": { - "value": { - "description": "Number of units administered.", - "type": "number", - "example": 1 - }, - "unit": { - "description": "Description of unit.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "System that defines coded unit form.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "Code describing the unit.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "Organisation that performed the immunisation.", - "type": "object", - "required": [ - "type", - "identifier" - ], - "properties": { - "type": { - "description": "Type of actor. Always `Organisation`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "Organisation identifier.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "Organisation's ODS code.", - "type": "string", - "example": "B0C4P" - } - } - }, - "display": { - "description": "Organisation that performed the immunisation.", - "type": "string", - "example": "UNIVERSITY HOSPITAL OF WALES" - } - } - } - } - } - }, - "reasonCode": { - "description": "Reasons why the vaccine was administered.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against.", - "items": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - }, - "required": [ - "system", - "code", - "display" - ] - } - } - }, - "required": [ - "coding" - ] - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Dose number within series. Can be an integer or string. Kindly, refer below elements", - "properties": { - "doseNumberPositiveInt": { - "description": "Dose number within a series of doses.", - "type": "integer", - "example": 1 - }, - "doseNumberString": { - "description": "A string should only be used in cases where an integer is not available.", - "type": "string" - } - }, - "required": [ - "doseNumberPositiveInt" - ] - } - } - } - } - } - }, - { - "description": "Demographic information about the patient receiving an immunisation.", - "type": "object", - "required": [ - "resourceType", - "id" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "id": { - "description": "Patient ID (NHS Number)", - "type": "string", - "example": "9000000009" - }, - "identifier": { - "description": "Unique identifier for this patient. Always an NHS number.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used to identify patients.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Code identifying the patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - } - } - ] - }, - "search": { - "description": "Search-related information for the Immunization.", - "type": "object", - "required": [ - "mode" - ], - "properties": { - "mode": { - "description": "Indicates why this resource is in the result set. For Immunization resources this is always `match`.", - "enum": [ - "match", - "include" - ] - } - } - } - } - } - }, - "total": { - "description": "Number of matching immunisations found.", - "type": "integer", - "example": 2 - } - } - }, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "link": [ - { - "relation": "self", - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=RSV&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9000000009" - } - ], - "entry": [ - { - "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", - "resource": { - "resourceType": "Immunization", - "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - }, - "display": "UNIVERSITY HOSPITAL OF WALES" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005", - "display": "Disease outbreak (event)" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "resource": { - "resourceType": "Patient", - "id": "9000000009", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ] - }, - "search": { - "mode": "include" - } - } - ], - "total": 1 - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms" - } - } - } - }, - "/Immunization/_search": { - "post": { - "summary": "Search (POST) for a patient's immunisation records", - "operationId": "searchViaPOSTImmunization", - "description": "## Overview\nYou may use this interaction as an alternative to a search with the GET verb. A POST search allows you to supply some or all parameters in the body of the request should you need to do so. It offers the same search functionality, see Search (GET) interaction for details.\n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\\|9000000009` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `patient.identifier` or -immunization.target or _include | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/PatientIdentifier" - }, - { - "$ref": "#/components/parameters/ImmunizationTarget" - }, - { - "$ref": "#/components/parameters/DateFrom" - }, - { - "$ref": "#/components/parameters/DateTo" - }, - { - "$ref": "#/components/parameters/Include" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/SearchImmunization" - }, - "responses": { - "200": { - "description": "Search immunisation operation successful", - "content": { - "application/fhir+json": { - "schema": { - "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", - "type": "object", - "required": [ - "resourceType", - "type", - "total", - "entry" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Bundle`.", - "type": "string", - "example": "Bundle" - }, - "type": { - "description": "Indicates how the bundle is intended to be used. Always `searchset`.", - "type": "string", - "example": "searchset" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "relation": { - "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1). Always `Self`.", - "type": "string" - }, - "url": { - "description": "A url representing the search applied by the API to generate the result which may differ from the request if unrecognised or unsupported parameters have been ignored.", - "type": "string" - } - }, - "required": [ - "relation", - "url" - ] - } - }, - "entry": { - "description": "List of matching immunisations and associated patient. If there were no matching immunisations, this is an empty list.", - "type": "array", - "items": { - "type": "object", - "required": [ - "fullUrl", - "resource", - "search" - ], - "properties": { - "fullUrl": { - "description": "URI for the Immunization or Patient resource.", - "type": "string", - "example": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "resource": { - "description": "The Immunization or Patient resource.", - "oneOf": [ - { - "description": "A matching immunisation, formatted as a FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrenceDateTime", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "Identifies where the resource comes from." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - } - } - } - } - } - }, - "identifier": { - "description": "Unique identifier for this immunisation record, as generated by the source system.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "URI of the namespace of this identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc" - }, - "value": { - "description": "Identifier value within `system`.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who was immunised.", - "type": "object", - "required": [ - "reference", - "type", - "identifier" - ], - "properties": { - "reference": { - "description": "URI for the associated Patient resource in the bundle.", - "type": "string", - "example": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" - }, - "type": { - "description": "Type of resource this reference refers to. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "identifier": { - "description": "Business identifier for linked Patient. Always an NHS number.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "URI of coding system used to identify linked patient. Always https://fhir.nhs.uk/Id/nhs-number", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Value in coding system representing linked patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - }, - "occurrenceDateTime": { - "description": "Date and time of immunisation.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "required": [ - "identifier" - ] - }, - "manufacturer": { - "description": "Vaccine manufacturer details.", - "type": "object", - "properties": { - "display": { - "description": "Decsription of the vaccine manufacturer.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Lot number of the vaccine product.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Date vaccine batch expires.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - }, - "required": [ - "coding" - ] - }, - "route": { - "description": "The path by which the vaccine product is taken into the body.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered.", - "type": "object", - "required": [], - "properties": { - "value": { - "description": "Number of units administered.", - "type": "number", - "example": 1 - }, - "unit": { - "description": "Description of unit.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "System that defines coded unit form.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "Code describing the unit.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "Organisation that performed the immunisation.", - "type": "object", - "required": [ - "type", - "identifier" - ], - "properties": { - "type": { - "description": "Type of actor. Always `Organisation`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "Organisation identifier.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "Organisation's ODS code.", - "type": "string", - "example": "B0C4P" - } - } - }, - "display": { - "description": "Organisation that performed the immunisation.", - "type": "string", - "example": "UNIVERSITY HOSPITAL OF WALES" - } - } - } - } - } - }, - "reasonCode": { - "description": "Reasons why the vaccine was administered.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against.", - "items": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - }, - "required": [ - "system", - "code", - "display" - ] - } - } - }, - "required": [ - "coding" - ] - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Dose number within series. Can be an integer or string. Kindly, refer below elements", - "properties": { - "doseNumberPositiveInt": { - "description": "Dose number within a series of doses.", - "type": "integer", - "example": 1 - }, - "doseNumberString": { - "description": "A string should only be used in cases where an integer is not available.", - "type": "string" - } - }, - "required": [ - "doseNumberPositiveInt" - ] - } - } - } - } - } - }, - { - "description": "Demographic information about the patient receiving an immunisation.", - "type": "object", - "required": [ - "resourceType", - "id" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "id": { - "description": "Patient ID (NHS Number)", - "type": "string", - "example": "9000000009" - }, - "identifier": { - "description": "Unique identifier for this patient. Always an NHS number.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used to identify patients.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Code identifying the patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - } - } - ] - }, - "search": { - "description": "Search-related information for the Immunization.", - "type": "object", - "required": [ - "mode" - ], - "properties": { - "mode": { - "description": "Indicates why this resource is in the result set. For Immunization resources this is always `match`.", - "enum": [ - "match", - "include" - ] - } - } - } - } - } - }, - "total": { - "description": "Number of matching immunisations found.", - "type": "integer", - "example": 2 - } - } - }, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "link": [ - { - "relation": "self", - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=RSV&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9000000009" - } - ], - "entry": [ - { - "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", - "resource": { - "resourceType": "Immunization", - "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - }, - "display": "UNIVERSITY HOSPITAL OF WALES" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005", - "display": "Disease outbreak (event)" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "resource": { - "resourceType": "Patient", - "id": "9000000009", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ] - }, - "search": { - "mode": "include" - } - } - ], - "total": 1 - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms" - } - } - } - }, - "/Immunization/{id}": { - "get": { - "summary": "Retrieve a record of an immunisation by its unique identifier", - "operationId": "readImmunization", - "description": "## Overview\nThis interaction allows you to retrieve the record of a single vaccination by our assigned id. We will return the full immunization resource as submitted. \nThe response will include an eTag for the version of the record which has been returned. If you intend to update a record, it is recommended that you use this interaction to obtain the latest version (and eTag for the version). \nTo retrieve a full vaccination history for a patient, see the search interaction. \nYou must be authorised for the read interaction and the disease type associated with the vaccination event in order to access the record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation record found | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `id` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "description": "Read Immunization operation successful", - "headers": { - "CorrelationID": { - "$ref": "#/components/parameters/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/parameters/RequestID" - }, - "E-Tag": { - "$ref": "#/components/parameters/E-Tag" - } - }, - "content": { - "application/fhir+json": { - "schema": { - "description": "A matching immunisation, formatted as a FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrenceDateTime", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "The schema for Practitioner & Patient are different. In response both Practitioner & Patient objects will be returned.", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "name": { - "type": "array", - "description": "A name associated with the patient", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "An address for the individual", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Postal code for area" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "identifier", - "name", - "gender", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - } - } - } - } - } - }, - "identifier": { - "description": "Unique identifier for this immunisation record, as generated by the source system.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "URI of the namespace of this identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc" - }, - "value": { - "description": "Identifier value within `system`.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who was immunised.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrenceDateTime": { - "description": "Date and time of immunisation.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "required": [ - "identifier" - ] - }, - "manufacturer": { - "description": "Vaccine manufacturer details.", - "type": "object", - "properties": { - "display": { - "description": "Decsription of the vaccine manufacturer.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Lot number of the vaccine product.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Date vaccine batch expires.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - }, - "required": [ - "coding" - ] - }, - "route": { - "description": "The path by which the vaccine product is taken into the body.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered.", - "type": "object", - "required": [], - "properties": { - "value": { - "description": "Number of units administered.", - "type": "number", - "example": 1 - }, - "unit": { - "description": "Description of unit.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "System that defines coded unit form.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "Code describing the unit.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "Organisation that performed the immunisation.", - "type": "object", - "required": [ - "type", - "identifier" - ], - "properties": { - "type": { - "description": "Type of actor. Always `Organisation`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "Organisation identifier.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "Organisation's ODS code.", - "type": "string", - "example": "B0C4P" - } - } - }, - "display": { - "description": "Organisation that performed the immunisation.", - "type": "string", - "example": "UNIVERSITY HOSPITAL OF WALES" - } - } - } - } - } - }, - "reasonCode": { - "description": "Reasons why the vaccine was administered.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against.", - "items": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - }, - "required": [ - "system", - "code", - "display" - ] - } - } - }, - "required": [ - "coding" - ] - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Dose number within series. Can be an integer or string. Kindly, refer below elements", - "properties": { - "doseNumberPositiveInt": { - "description": "Dose number within a series of doses.", - "type": "integer", - "example": 1 - }, - "doseNumberString": { - "description": "A string should only be used in cases where an integer is not available.", - "type": "string" - } - }, - "required": [ - "doseNumberPositiveInt" - ] - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Owl", - "given": [ - "Barney" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Owler", - "given": [ - "Ozzie" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms", - "example": { - "resourceType": "OperationOutcome", - "id": "3d64df5a-b753-49ec-b3df-f45c157941eb", - "issue": [ - { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", - "code": "INVALID" - } - ] - }, - "diagnostics": "The provided event ID is either missing or not in the expected format." - } - ] - } - } - } - }, - "put": { - "summary": "Update a record of vaccination", - "operationId": "updateImmunization", - "description": "## Overview\nThis record allows you to add a new updated record of a vaccination event. Update replaces the full immunization resource, so you must provide all data fields, not just the change (Patch is not currently supported). You may obtain all the current data for the vaccination event using the read interaction, which will also return an eTag for the version. \nYou may use update to re-instate a deleted record, but our update interaction does not support creating a new vaccination event where one does not currently exist. \nYou must not change the identifier when updating a vaccination event. The identifier is used as a primary identifier by downstream systems. \nYou must be authorised for update interaction and the disease type associated with the vaccination event in order to update the record. \n\n## Sandbox testing \n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Update a vaccination event | valid request as per schema | HTTP Status 200 |\n| Bad Request(missing/invalid required element in request body) | Didn't pass `E-Tag` in request header | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - }, - { - "$ref": "#/components/parameters/E-Tag" - } - ], - "requestBody": { - "content": { - "application/fhir+json": { - "schema": { - "description": "A FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "id", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrenceDateTime", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - } - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "name": { - "type": "array", - "description": "Patient name as registered on PDS or as recorded by the user where the record cannot be traced on PDS. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "identifier", - "name", - "gender", - "birthDate", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - }, - "text": { - "description": "Plain text representation of the concept.", - "type": "string" - } - } - } - } - } - }, - "identifier": { - "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "A URI for the system that has allocated the vaccination identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" - }, - "value": { - "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrenceDateTime": { - "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \nOnly positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", - "type": "string", - "example": "urn:iso:std:iso:3166" - }, - "value": { - "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", - "type": "string", - "example": "GB" - } - }, - "required": [ - "system", - "value" - ] - } - } - }, - "manufacturer": { - "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", - "type": "object", - "properties": { - "display": { - "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - } - }, - "route": { - "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", - "type": "object", - "required": [], - "properties": { - "value": { - "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", - "type": "number", - "example": 1 - }, - "unit": { - "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", - "type": "object", - "properties": { - "type": { - "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", - "type": "string", - "example": "B0C4P" - } - } - }, - "reference": { - "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", - "type": "string", - "example": "#Pract1" - } - } - } - } - } - }, - "reasonCode": { - "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", - "type": "array", - "items": { - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. A code list will be provided for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - } - } - } - } - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. The use of an integer is preferred if known. A string should only be used in cases where an integer is not available.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available.", - "type": "integer", - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "4ff607e0-c6e9-4fe0-a2b6-3bcd7fdc44c9", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "a7437179-e86e-4855-b68e-24b5jhg3g" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07T13:28:17.271+00:00", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Update Immunization operation successful", - "headers": { - "CorrelationID": { - "$ref": "#/components/parameters/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/parameters/RequestID" - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms" - } - } - }, - "delete": { - "summary": "Mark a record of vaccination as being entered in error", - "operationId": "deleteImmunization", - "description": "## Overview\nThis endpoint allows you to mark a record that has been entered in error.\nDeleted records will continue to be stored for a period of time but are not returned in response to read or search requests.\nA deleted record can be re-instated using the update interaction if it was incorrectly deleted. \n\n## Sandbox testing\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Delete a vaccination event | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 204 No Content |\n| Bad Request | Didn't pass Required fields `id` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "204": { - "description": "Delete Immunization operation successful" - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms" - } - } - } - }, - "components": { - "responses": { - "4XX-imms": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | INVALID | The provided event ID is either missing or not in the expected format. | {\"resourceType\": \"OperationOutcome\", \"id\": \"6f4ca309-19d7-4f61-90b3-acbd1f2eb8f8\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 400 | BAD_REQUEST | Search could not be processed or failed basic FHIR validation rules | {\"resourceType\": \"OperationOutcome\", \"id\": \"4ff75db4-6e7d-411d-a490-c55c11f83043\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"patient.identifier must be in the format of \\\"https://fhir.nhs.uk/Id/nhs-number|{NHS number}\\\" e.g. \\\"https://fhir.nhs.uk/Id/nhs-number|9000000009\\\"\"}]} |\n| 401 | UNAUTHORISED | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | UNAUTHORISED | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 404 | NOT_FOUND | The requested resource was not found. | {\"resourceType\": \"OperationOutcome\", \"id\": \"bc2c3c82-4392-4314-9d6b-a7345f82d923\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"The requested resource was not found.\"}]} |\n| 405 | NOT_ALLOWED | The requested method is not allowed |\n| 422 | UNPROCESSABLE_ENTITY | The proposed resource violated applicable FHIR profiles or server business rules. This should be accompanied by an OperationOutcome resource providing additional detail |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType": "OperationOutcome", - "id": "3d64df5a-b753-49ec-b3df-f45c157941eb", - "issue": [ - { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", - "code": "INVALID" - } - ] - }, - "diagnostics": "Search parameter patient.identifier/-immunization.target must have one value." - } - ] - } - } - } - } - }, - "requestBodies": { - "Immunization": { - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/Immunization" - } - } - }, - "required": true - }, - "SearchImmunization": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "type": "object", - "properties": { - "patient.identifier": { - "type": "string", - "description": "The patient's NHS number.\nExpressed as `` where`` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", - "example": "9000000009" - }, - "-immunization.target": { - "type": "string", - "description": "Specific procedures, disorders, diseases, infections or organisms.\n", - "enum": [ - "COVID19", - "FLU", - "RSV" - ] - }, - "-date.from": { - "type": "string", - "format": "date", - "description": "The earliest date to be included (e.g. 2020-01-01)", - "default": "1900-01-01" - }, - "-date.to": { - "type": "string", - "format": "date", - "description": "The latest date to be included (e.g. 2020-12-31)", - "default": "9999-12-31" - }, - "_include": { - "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", - "type": "string", - "default": "Immunization:patient" - } - } - } - } - } - } - }, - "parameters": { - "Id": { - "in": "path", - "name": "id", - "required": true, - "description": "A required ID which you can use to identify an Immunization event object.\n\nMirrored back in a response header.\n", - "schema": { - "type": "string", - "example": "29dc4e84-7e72-11ee-b962-0242ac120002" - } - }, - "CorrelationID": { - "in": "header", - "name": "X-Correlation-ID", - "required": false, - "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "RequestID": { - "in": "header", - "name": "X-Request-ID", - "required": false, - "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "PatientIdentifier": { - "in": "query", - "name": "patient.identifier", - "description": "The patient's NHS number.\nExpressed as `|` where `` must be `https://fhir.nhs.uk/Id/nhs-number` and `` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", - "required": true, - "schema": { - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number|9000000009" - } - }, - "ImmunizationTarget": { - "in": "query", - "name": "-immunization.target", - "description": "Specific procedures, disorders, diseases, infections or organisms.\n", - "required": true, - "schema": { - "type": "string", - "enum": [ - "COVID19", - "FLU", - "RSV" - ] - } - }, - "DateFrom": { - "in": "query", - "name": "-date.from", - "description": "The earliest date to be included (e.g. 2020-01-01)", - "schema": { - "type": "string", - "format": "date", - "default": "1900-01-01" - } - }, - "DateTo": { - "in": "query", - "name": "-date.to", - "description": "The latest date to be included (e.g. 2020-12-31)", - "schema": { - "type": "string", - "format": "date", - "default": "9999-12-31" - } - }, - "Include": { - "in": "query", - "name": "_include", - "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", - "required": false, - "schema": { - "type": "string", - "default": "Immunization:patient" - } - }, - "Location": { - "in": "header", - "name": "location", - "required": true, - "description": "The location of newly created Immunization record. It contains resouce ID at the end.", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "E-Tag": { - "in": "header", - "name": "E-Tag", - "required": true, - "description": "Indicaes the current version of Immunization resource", - "schema": { - "type": "integer", - "example": 1 - } - } - }, - "schemas": { - "Resource": { - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "enum": [ - "Resource", - "DomainResource", - "Account", - "ActivityDefinition", - "AdverseEvent", - "AllergyIntolerance", - "Appointment", - "AppointmentResponse", - "AuditEvent", - "Basic", - "Binary", - "BiologicallyDerivedProduct", - "BodyStructure", - "Bundle", - "CapabilityStatement", - "CarePlan", - "CareTeam", - "CatalogEntry", - "ChargeItem", - "ChargeItemDefinition", - "Claim", - "ClaimResponse", - "ClinicalImpression", - "CodeSystem", - "Communication", - "CommunicationRequest", - "CompartmentDefinition", - "Composition", - "ConceptMap", - "Condition", - "Consent", - "Contract", - "Coverage", - "CoverageEligibilityRequest", - "CoverageEligibilityResponse", - "DetectedIssue", - "Device", - "DeviceDefinition", - "DeviceMetric", - "DeviceRequest", - "DeviceUseStatement", - "DiagnosticReport", - "DocumentManifest", - "DocumentReference", - "EffectEvidenceSynthesis", - "Encounter", - "Endpoint", - "EnrollmentRequest", - "EnrollmentResponse", - "EpisodeOfCare", - "EventDefinition", - "Evidence", - "EvidenceVariable", - "ExampleScenario", - "ExplanationOfBenefit", - "FamilyMemberHistory", - "Flag", - "Goal", - "GraphDefinition", - "Group", - "GuidanceResponse", - "HealthcareService", - "ImagingStudy", - "Immunization", - "ImmunizationEvaluation", - "ImmunizationRecommendation", - "ImplementationGuide", - "InsurancePlan", - "Invoice", - "Library", - "Linkage", - "List", - "Location", - "Measure", - "MeasureReport", - "Media", - "Medication", - "MedicationAdministration", - "MedicationDispense", - "MedicationKnowledge", - "MedicationRequest", - "MedicationStatement", - "MedicinalProduct", - "MedicinalProductAuthorization", - "MedicinalProductContraindication", - "MedicinalProductIndication", - "MedicinalProductIngredient", - "MedicinalProductInteraction", - "MedicinalProductManufactured", - "MedicinalProductPackaged", - "MedicinalProductPharmaceutical", - "MedicinalProductUndesirableEffect", - "MessageDefinition", - "MessageHeader", - "MolecularSequence", - "NamingSystem", - "NutritionOrder", - "Observation", - "ObservationDefinition", - "OperationDefinition", - "OperationOutcome", - "Organization", - "OrganizationAffiliation", - "Parameters", - "Patient", - "PaymentNotice", - "PaymentReconciliation", - "Person", - "PlanDefinition", - "Practitioner", - "PractitionerRole", - "Procedure", - "Provenance", - "Questionnaire", - "QuestionnaireResponse", - "RelatedPerson", - "RequestGroup", - "ResearchDefinition", - "ResearchElementDefinition", - "ResearchStudy", - "ResearchSubject", - "RiskAssessment", - "RiskEvidenceSynthesis", - "Schedule", - "SearchParameter", - "ServiceRequest", - "Slot", - "Specimen", - "SpecimenDefinition", - "StructureDefinition", - "StructureMap", - "Subscription", - "Substance", - "SubstanceNucleicAcid", - "SubstancePolymer", - "SubstanceProtein", - "SubstanceReferenceInformation", - "SubstanceSourceMaterial", - "SubstanceSpecification", - "SupplyDelivery", - "SupplyRequest", - "Task", - "TerminologyCapabilities", - "TestReport", - "TestScript", - "ValueSet", - "VerificationResult", - "VisionPrescription" - ] - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - }, - "DomainResource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "enum": [ - "Resource", - "DomainResource", - "Account", - "ActivityDefinition", - "AdverseEvent", - "AllergyIntolerance", - "Appointment", - "AppointmentResponse", - "AuditEvent", - "Basic", - "Binary", - "BiologicallyDerivedProduct", - "BodyStructure", - "Bundle", - "CapabilityStatement", - "CarePlan", - "CareTeam", - "CatalogEntry", - "ChargeItem", - "ChargeItemDefinition", - "Claim", - "ClaimResponse", - "ClinicalImpression", - "CodeSystem", - "Communication", - "CommunicationRequest", - "CompartmentDefinition", - "Composition", - "ConceptMap", - "Condition", - "Consent", - "Contract", - "Coverage", - "CoverageEligibilityRequest", - "CoverageEligibilityResponse", - "DetectedIssue", - "Device", - "DeviceDefinition", - "DeviceMetric", - "DeviceRequest", - "DeviceUseStatement", - "DiagnosticReport", - "DocumentManifest", - "DocumentReference", - "EffectEvidenceSynthesis", - "Encounter", - "Endpoint", - "EnrollmentRequest", - "EnrollmentResponse", - "EpisodeOfCare", - "EventDefinition", - "Evidence", - "EvidenceVariable", - "ExampleScenario", - "ExplanationOfBenefit", - "FamilyMemberHistory", - "Flag", - "Goal", - "GraphDefinition", - "Group", - "GuidanceResponse", - "HealthcareService", - "ImagingStudy", - "Immunization", - "ImmunizationEvaluation", - "ImmunizationRecommendation", - "ImplementationGuide", - "InsurancePlan", - "Invoice", - "Library", - "Linkage", - "List", - "Location", - "Measure", - "MeasureReport", - "Media", - "Medication", - "MedicationAdministration", - "MedicationDispense", - "MedicationKnowledge", - "MedicationRequest", - "MedicationStatement", - "MedicinalProduct", - "MedicinalProductAuthorization", - "MedicinalProductContraindication", - "MedicinalProductIndication", - "MedicinalProductIngredient", - "MedicinalProductInteraction", - "MedicinalProductManufactured", - "MedicinalProductPackaged", - "MedicinalProductPharmaceutical", - "MedicinalProductUndesirableEffect", - "MessageDefinition", - "MessageHeader", - "MolecularSequence", - "NamingSystem", - "NutritionOrder", - "Observation", - "ObservationDefinition", - "OperationDefinition", - "OperationOutcome", - "Organization", - "OrganizationAffiliation", - "Parameters", - "Patient", - "PaymentNotice", - "PaymentReconciliation", - "Person", - "PlanDefinition", - "Practitioner", - "PractitionerRole", - "Procedure", - "Provenance", - "Questionnaire", - "QuestionnaireResponse", - "RelatedPerson", - "RequestGroup", - "ResearchDefinition", - "ResearchElementDefinition", - "ResearchStudy", - "ResearchSubject", - "RiskAssessment", - "RiskEvidenceSynthesis", - "Schedule", - "SearchParameter", - "ServiceRequest", - "Slot", - "Specimen", - "SpecimenDefinition", - "StructureDefinition", - "StructureMap", - "Subscription", - "Substance", - "SubstanceNucleicAcid", - "SubstancePolymer", - "SubstanceProtein", - "SubstanceReferenceInformation", - "SubstanceSourceMaterial", - "SubstanceSpecification", - "SupplyDelivery", - "SupplyRequest", - "Task", - "TerminologyCapabilities", - "TestReport", - "TestScript", - "ValueSet", - "VerificationResult", - "VisionPrescription" - ] - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - } - } - }, - "Immunization": { - "type": "object", - "required": [ - "status", - "vaccineCode", - "patient" - ], - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "example": "Immunization" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - }, - "identifier": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Identifier", - "description": "A unique identifier assigned to this immunization record." - } - }, - "status": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Indicates the current status of the immunization event." - }, - "statusReason": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates the reason the immunization event was not performed." - }, - "vaccineCode": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Vaccine that was administered or was to be administered." - }, - "patient": { - "$ref": "#/components/schemas/Reference", - "description": "The patient who either received or did not receive the immunization." - }, - "encounter": { - "$ref": "#/components/schemas/Reference", - "description": "The visit or admission or other contact between patient and health care provider the immunization was performed as part of." - }, - "occurrenceDateTime": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date vaccine administered or was to be administered." - }, - "occurrenceString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Date vaccine administered or was to be administered." - }, - "recorded": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event." - }, - "primarySource": { - "type": "boolean", - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded." - }, - "reportOrigin": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The source of the data when the report of the immunization event is not based on information from the person who administered the vaccine." - }, - "location": { - "$ref": "#/components/schemas/Reference", - "description": "The service delivery location where the vaccine administration occurred." - }, - "manufacturer": { - "$ref": "#/components/schemas/Reference", - "description": "Name of vaccine manufacturer." - }, - "lotNumber": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Lot number of the vaccine product." - }, - "expirationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", - "description": "Date vaccine batch expires." - }, - "site": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Body site where vaccine was administered." - }, - "route": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The path by which the vaccine product is taken into the body." - }, - "doseQuantity": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The quantity of vaccine product that was administered." - }, - "performer": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Performer", - "description": "Indicates who performed the immunization event." - } - }, - "note": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Annotation", - "description": "Extra information about the immunization that is not conveyed by the other attributes." - } - }, - "reasonCode": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Reasons why the vaccine was administered." - } - }, - "reasonReference": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Reference", - "description": "Condition, Observation or DiagnosticReport that supports why the immunization was administered." - } - }, - "isSubpotent": { - "type": "boolean", - "description": "Indication if a dose is considered to be subpotent. By default, a dose should be considered to be potent." - }, - "subpotentReason": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Reason why a dose is considered to be subpotent." - } - }, - "education": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Education", - "description": "Educational material presented to the patient (or guardian) at the time of vaccine administration." - } - }, - "programEligibility": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates a patient's eligibility for a funding program." - } - }, - "fundingSource": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates the source of the vaccine actually administered. This may be different than the patient eligibility (e.g. the patient may be eligible for a publically purchased vaccine but due to inventory issues, vaccine purchased with private funds was actually administered)." - }, - "reaction": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Reaction", - "description": "Categorical data indicating that an adverse event is associated in time to an immunization." - } - }, - "protocolApplied": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_ProtocolApplied", - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose." - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Owl", - "given": [ - "Barney" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Owler", - "given": [ - "Ozzie" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "ZZ99 3CZ" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "urn:iso:std:iso:3166", - "value": "GB" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "N2N9I" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - }, - "Bundle": { - "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", - "type": "object", - "required": [ - "resourceType", - "type" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Bundle`.", - "type": "string", - "example": "Bundle" - }, - "type": { - "description": "Indicates how the bundle is intended to be used. Always `searchset`.", - "type": "string", - "example": "searchset" - }, - "link": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Link", - "description": "A series of links that provide context to this bundle." - } - }, - "entry": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Entry", - "description": "An entry in a bundle resource - will either contain a resource or information about a resource (transactions and history only)." - } - }, - "total": { - "type": "integer", - "format": "int32", - "description": "If a set of search matches, this is the total number of entries of type 'match' across all pages in the search. It does not include search.mode = 'include' or 'outcome' entries and it does not provide a count of the number of entries in the Bundle." - } - }, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "link": [ - { - "relation": "self", - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=COVID19&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9449306206" - } - ], - "entry": [ - { - "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", - "resource": { - "resourceType": "Immunization", - "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449306206" - } - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "urn:iso:std:iso:3166", - "value": "GB" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - }, - "display": "UNIVERSITY HOSPITAL OF WALES" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005", - "display": "Disease outbreak (event)" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "resource": { - "resourceType": "Patient", - "id": "9449306206", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449306206" - } - ], - "birthDate": "2014-03-25" - }, - "search": { - "mode": "include" - } - } - ], - "total": 1 - } - }, - "OperationOutcome": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "example": "OperationOutcome" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "issue": { - "type": "array", - "items": { - "type": "object", - "properties": { - "severity": { - "type": "string" - }, - "code": { - "type": "string" - }, - "details": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string" - }, - "code": { - "type": "string" - } - } - } - } - } - }, - "diagnostics": { - "type": "string" - } - } - } - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - }, - "issue": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OperationOutcome_Issue", - "description": "An error, warning, or information message that results from a system action." - }, - "minItems": 1 - } - }, - "required": [ - "issue" - ], - "example": { - "resourceType": "OperationOutcome", - "meta": { - "versionId": "BnpJOa5-Sb", - "lastUpdated": "2021-04-12T14:34:36.061-05:00", - "source": "BCL3d5NERb", - "profile": [ - "xSempdez3Y" - ], - "security": [ - { - "system": "tczS7uP8XL", - "version": "IXKbCw05qO", - "code": "NvDP1hL64Y", - "display": "_r1z5oJld1", - "userSelected": true - } - ], - "tag": [ - { - "system": "2qqXHsE1Mx", - "version": "lybFyQ1tBj", - "code": "Q9w075fYd3", - "display": "Nm2QqbYibP", - "userSelected": true - }, - { - "code": "ibm/complete-mock" - } - ] - }, - "implicitRules": "l8KHk6qOt4", - "language": "en-US", - "text": { - "status": "additional", - "div": "
" - }, - "issue": [ - { - "severity": "warning", - "code": "business-rule", - "details": { - "coding": [ - { - "system": "eQgFofRHmJ", - "version": "T524HDk5Za", - "code": "tC_7iQg31j", - "display": "s0bLc4W5KE", - "userSelected": true - } - ], - "text": "BfBVppHmsh" - }, - "diagnostics": "EcvPDGbK0q", - "location": [ - "GK5ihTmfe6" - ], - "expression": [ - "Uidx_swV4Z" - ] - } - ] - } - }, - "Bundle_Entry": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "link": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Link", - "description": "A series of links that provide context to this entry." - } - }, - "fullUrl": { - "type": "string", - "pattern": "\\S*", - "description": "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource - i.e. if the fullUrl is not a urn:uuid, the URL shall be version-independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified." - }, - "resource": { - "description": "The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type." - }, - "resourceType": { - "type": "string", - "example": "Immunization" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - }, - "search": { - "$ref": "#/components/schemas/Bundle_Entry_Search", - "description": "Information about the search process that lead to the creation of this entry." - }, - "request": { - "$ref": "#/components/schemas/Bundle_Entry_Request", - "description": "Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry." - }, - "response": { - "$ref": "#/components/schemas/Bundle_Entry_Response", - "description": "Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history." - } - } - } - ] - }, - "Bundle_Entry_Response": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The status code returned by processing this entry. The status SHALL start with a 3 digit HTTP code (e.g. 404) and may contain the standard HTTP description associated with the status code." - }, - "location": { - "type": "string", - "pattern": "\\S*", - "description": "The location header created by processing this operation, populated if the operation returns a location." - }, - "etag": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The Etag for the resource, if the operation for the entry produced a versioned resource (see [Resource Metadata and Versioning](http.html#versioning) and [Managing Resource Contention](http.html#concurrency))." - }, - "lastModified": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "The date/time that the resource was modified on the server." - }, - "outcome": { - "$ref": "#/components/schemas/Resource", - "description": "An OperationOutcome containing hints and warnings produced as part of processing this entry in a batch or transaction." - } - }, - "required": [ - "status" - ] - } - ] - }, - "Bundle_Entry_Request": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "method": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "In a transaction or batch, this is the HTTP action to be executed for this entry. In a history bundle, this indicates the HTTP action that occurred." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "The URL for this entry, relative to the root (the address to which the request is posted)." - }, - "ifNoneMatch": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "If the ETag values match, return a 304 Not Modified status. See the API documentation for [\"Conditional Read\"](http.html#cread)." - }, - "ifModifiedSince": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "Only perform the operation if the last updated date matches. See the API documentation for [\"Conditional Read\"](http.html#cread)." - }, - "ifMatch": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Only perform the operation if the Etag value matches. For more information, see the API section [\"Managing Resource Contention\"](http.html#concurrency)." - }, - "ifNoneExist": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Instruct the server not to perform the create if a specified resource already exists. For further information, see the API documentation for [\"Conditional Create\"](http.html#ccreate). This is just the query portion of the URL - what follows the \"?\" (not including the \"?\")." - } - }, - "required": [ - "method", - "url" - ] - } - ] - }, - "Bundle_Entry_Search": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "mode": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Why this entry is in the result set - whether it's included as a match or because of an _include requirement, or to convey information or warning information about the search process." - }, - "score": { - "type": "number", - "description": "When searching, the server's search ranking score for the entry." - } - } - } - ] - }, - "Bundle_Link": { - "allOf": [ - { - "type": "object", - "properties": { - "relation": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1)." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "The reference details for the link." - } - }, - "required": [ - "relation", - "url" - ] - } - ] - }, - "Immunization_ProtocolApplied": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "series": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "One possible path to achieve presumed immunity against a disease - within the context of an authority." - }, - "authority": { - "$ref": "#/components/schemas/Reference", - "description": "Indicates the authority who published the protocol (e.g. ACIP) that is being followed." - }, - "targetDisease": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The vaccine preventable disease the dose is being administered against." - } - }, - "doseNumberPositiveInt": { - "type": "integer", - "format": "int32", - "description": "Nominal position in a series." - }, - "doseNumberString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Nominal position in a series." - }, - "seriesDosesPositiveInt": { - "type": "integer", - "format": "int32", - "description": "The recommended number of doses to achieve immunity." - }, - "seriesDosesString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The recommended number of doses to achieve immunity." - } - } - } - ] - }, - "Immunization_Reaction": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "date": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date of reaction to the immunization." - }, - "detail": { - "$ref": "#/components/schemas/Reference", - "description": "Details of the reaction." - }, - "reported": { - "type": "boolean", - "description": "Self-reported indicator." - } - } - } - ] - }, - "Immunization_Education": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "documentType": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Identifier of the material presented to the patient." - }, - "reference": { - "type": "string", - "pattern": "\\S*", - "description": "Reference pointer to the educational material given to the patient if the information was on line." - }, - "publicationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date the educational material was published." - }, - "presentationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date the educational material was given to the patient." - } - } - } - ] - }, - "Immunization_Performer": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "function": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Describes the type of performance (e.g. ordering provider, administering provider, etc.)." - }, - "actor": { - "$ref": "#/components/schemas/Reference", - "description": "The practitioner or organization who performed the action." - } - }, - "required": [ - "actor" - ] - } - ] - }, - "OperationOutcome_Issue": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "severity": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Indicates whether the issue indicates a variation from successful processing." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." - }, - "details": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Additional details about the error. This may be a text description of the error or a system code that identifies the error." - }, - "diagnostics": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Additional diagnostic information about the issue." - }, - "location": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "This element is deprecated because it is XML specific. It is replaced by issue.expression, which is format independent, and simpler to parse. \n\nFor resource issues, this will be a simple XPath limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised. For HTTP errors, will be \"http.\" + the parameter name." - } - }, - "expression": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A [simple subset of FHIRPath](fhirpath.html#simple) limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised." - } - } - }, - "required": [ - "severity", - "code" - ] - } - ] - }, - "Element": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - }, - "example": [ - { - "url": "http://example.com", - "valueString": "text value" - } - ] - } - } - }, - "BackboneElement": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - } - } - }, - "Address": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The purpose of this address." - }, - "type": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Distinguishes between physical addresses (those you can visit) and mailing addresses (e.g. PO Boxes and care-of addresses). Most addresses are both." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Specifies the entire address as it should be displayed e.g. on a postal label. This may be provided instead of or as well as the specific parts." - }, - "line": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "This component contains the house number, apartment number, street name, street direction, P.O. Box number, delivery hints, and similar address information." - } - }, - "city": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of the city, town, suburb, village or other community or delivery center." - }, - "district": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of the administrative area (county)." - }, - "state": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Sub-unit of a country with limited sovereignty in a federally organized country. A code may be used if codes are in common use (e.g. US 2 letter state codes)." - }, - "postalCode": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A postal code designating a region defined by the postal service." - }, - "country": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Country - a nation as commonly understood or generally accepted." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period when address was/is in use." - } - } - } - ] - }, - "Age": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Annotation": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "authorReference": { - "$ref": "#/components/schemas/Reference", - "description": "The individual responsible for making the annotation." - }, - "authorString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The individual responsible for making the annotation." - }, - "time": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Indicates when this particular annotation was made." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The text of the annotation in markdown format." - } - }, - "required": [ - "text" - ] - } - ] - }, - "Attachment": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The human language of the content. The value can be any valid value according to BCP 47." - }, - "data": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The actual data of the attachment - a sequence of bytes, base64 encoded." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "A location where the data can be accessed." - }, - "size": { - "type": "integer", - "format": "int32", - "description": "The number of bytes of data that make up this attachment (before base64 encoding, if that is done)." - }, - "hash": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The calculated hash of the data using SHA-1. Represented using base64." - }, - "title": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A label or set of text to display in place of the data." - }, - "creation": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The date that the attachment was first created." - } - } - } - ] - }, - "CodeableConcept": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "coding": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "A reference to a code defined by a terminology system." - } - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user." - } - } - }, - "Coding": { - "type": "object", - "properties": { - "system": { - "type": "string", - "pattern": "\\S*", - "description": "The identification of the code system that defines the meaning of the symbol in the code." - }, - "version": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination)." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A representation of the meaning of the code in the system, following the rules of the system." - }, - "userSelected": { - "type": "boolean", - "description": "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays)." - } - } - }, - "ContactPoint": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "system": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Telecommunications form for contact point - what communications system is required to make use of the contact." - }, - "value": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The actual contact point details, in a form that is meaningful to the designated communication system (i.e. phone number or email address)." - }, - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the purpose for the contact point." - }, - "rank": { - "type": "integer", - "format": "int32", - "description": "Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period when the contact point was/is in use." - } - } - } - ] - }, - "Count": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Distance": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Duration": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "HumanName": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the purpose for this name." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts." - }, - "family": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father." - }, - "given": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Given name." - } - }, - "prefix": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name." - } - }, - "suffix": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name." - } - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Indicates the period of time when this name was valid for the named person." - } - } - } - ] - }, - "Identifier": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The purpose of this identifier." - }, - "type": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A coded type for the identifier that can be used to determine which identifier to use for a specific purpose." - }, - "system": { - "type": "string", - "pattern": "\\S*", - "description": "Establishes the namespace for the value - that is, a URL that describes a set values that are unique." - }, - "value": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The portion of the identifier typically relevant to the user and which is unique within the context of the system." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period during which identifier is/was valid for use." - }, - "assigner": { - "$ref": "#/components/schemas/Reference", - "description": "Organization that issued/manages the identifier.", - "example": { - "reference": "Organization/123", - "type": "Organization", - "display": "The Assigning Organization" - } - } - } - }, - "Money": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "description": "Numerical value (with implicit precision)." - }, - "currency": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "ISO 4217 Currency Code." - } - } - } - ] - }, - "MoneyQuantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Period": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "start": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The start of the period. The boundary is inclusive." - }, - "end": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time." - } - } - } - ] - }, - "Quantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "description": "The value of the measured amount. The value includes an implicit precision in the presentation of the value." - }, - "comparator": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "How the value should be understood and represented - whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is \"<\" , then the real value is < stated value." - }, - "unit": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A human-readable form of the unit." - }, - "system": { - "type": "string", - "pattern": "\\S*", - "description": "The identification of the system that provides the coded form of the unit." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A computer processable form of the unit in some unit representation system." - } - } - } - ] - }, - "Range": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "low": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The low limit. The boundary is inclusive." - }, - "high": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The high limit. The boundary is inclusive." - } - } - } - ] - }, - "Ratio": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "numerator": { - "$ref": "#/components/schemas/Quantity", - "description": "The value of the numerator." - }, - "denominator": { - "$ref": "#/components/schemas/Quantity", - "description": "The value of the denominator." - } - } - } - ] - }, - "Reference": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "reference": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources." - }, - "type": { - "type": "string", - "pattern": "\\S*", - "description": "The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent.\n\nThe type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. \"Patient\" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources)." - }, - "identifier": { - "$ref": "#/components/schemas/Identifier", - "description": "An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Plain text narrative that identifies the resource in addition to the resource reference." - } - } - }, - "SampledData": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "origin": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The base quantity that a measured value of zero represents. In addition, this provides the units of the entire measurement series." - }, - "period": { - "type": "number", - "description": "The length of time between sampling times, measured in milliseconds." - }, - "factor": { - "type": "number", - "description": "A correction factor that is applied to the sampled data points before they are added to the origin." - }, - "lowerLimit": { - "type": "number", - "description": "The lower limit of detection of the measured points. This is needed if any of the data points have the value \"L\" (lower than detection limit)." - }, - "upperLimit": { - "type": "number", - "description": "The upper limit of detection of the measured points. This is needed if any of the data points have the value \"U\" (higher than detection limit)." - }, - "dimensions": { - "type": "integer", - "format": "int32", - "description": "The number of sample points at each time point. If this value is greater than one, then the dimensions will be interlaced - all the sample points for a point in time will be recorded at once." - }, - "data": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A series of data points which are decimal values separated by a single space (character u20). The special values \"E\" (error), \"L\" (below detection limit) and \"U\" (above detection limit) can also be used in place of a decimal value." - } - }, - "required": [ - "origin", - "period", - "dimensions" - ] - } - ] - }, - "SimpleQuantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Signature": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "type": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "An indication of the reason that the entity signed this document. This may be explicitly included as part of the signature information and can be used when determining accountability for various actions concerning the document." - }, - "minItems": 1 - }, - "when": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the digital signature was signed." - }, - "who": { - "$ref": "#/components/schemas/Reference", - "description": "A reference to an application-usable description of the identity that signed (e.g. the signature used their private key)." - }, - "onBehalfOf": { - "$ref": "#/components/schemas/Reference", - "description": "A reference to an application-usable description of the identity that is represented by the signature." - }, - "targetFormat": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A mime type that indicates the technical format of the target resources signed by the signature." - }, - "sigFormat": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A mime type that indicates the technical format of the signature. Important mime types are application/signature+xml for X ML DigSig, application/jose for JWS, and image/* for a graphical image of a signature, etc." - }, - "data": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The base64 encoding of the Signature content. When signature is not recorded electronically this element would be empty." - } - }, - "required": [ - "type", - "when", - "who" - ] - } - ] - }, - "Timing": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "event": { - "type": "array", - "items": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Identifies specific times when the event occurs." - } - }, - "repeat": { - "$ref": "#/components/schemas/Timing_Repeat", - "description": "A set of rules that describe when the event is scheduled." - }, - "code": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A code for the timing schedule (or just text in code.text). Some codes such as BID are ubiquitous, but many institutions define their own additional codes. If a code is provided, the code is understood to be a complete statement of whatever is specified in the structured timing data, and either the code or the data may be used to interpret the Timing, with the exception that .repeat.bounds still applies over the code (and is not contained in the code)." - } - } - } - ] - }, - "Timing_Repeat": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "boundsDuration": { - "$ref": "#/components/schemas/Duration", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "boundsRange": { - "$ref": "#/components/schemas/Range", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "boundsPeriod": { - "$ref": "#/components/schemas/Period", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "count": { - "type": "integer", - "format": "int32", - "description": "A total count of the desired number of repetitions across the duration of the entire timing specification. If countMax is present, this element indicates the lower bound of the allowed range of count values." - }, - "countMax": { - "type": "integer", - "format": "int32", - "description": "If present, indicates that the count is a range - so to perform the action between [count] and [countMax] times." - }, - "duration": { - "type": "number", - "description": "How long this thing happens for when it happens. If durationMax is present, this element indicates the lower bound of the allowed range of the duration." - }, - "durationMax": { - "type": "number", - "description": "If present, indicates that the duration is a range - so to perform the action between [duration] and [durationMax] time length." - }, - "durationUnit": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The units of time for the duration, in UCUM units." - }, - "frequency": { - "type": "integer", - "format": "int32", - "description": "The number of times to repeat the action within the specified period. If frequencyMax is present, this element indicates the lower bound of the allowed range of the frequency." - }, - "frequencyMax": { - "type": "integer", - "format": "int32", - "description": "If present, indicates that the frequency is a range - so to repeat between [frequency] and [frequencyMax] times within the period or period range." - }, - "period": { - "type": "number", - "description": "Indicates the duration of time over which repetitions are to occur; e.g. to express \"3 times per day\", 3 would be the frequency and \"1 day\" would be the period. If periodMax is present, this element indicates the lower bound of the allowed range of the period length." - }, - "periodMax": { - "type": "number", - "description": "If present, indicates that the period is a range from [period] to [periodMax], allowing expressing concepts such as \"do this once every 3-5 days." - }, - "periodUnit": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The units of time for the period in UCUM units." - }, - "dayOfWeek": { - "type": "array", - "items": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "If one or more days of week is provided, then the action happens only on the specified day(s)." - } - }, - "timeOfDay": { - "type": "array", - "items": { - "type": "string", - "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", - "description": "Specified time of day for action to take place." - } - }, - "when": { - "type": "array", - "items": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "An approximate time period during the day, potentially linked to an event of daily living that indicates when the action should occur." - } - }, - "offset": { - "type": "integer", - "format": "int32", - "description": "The number of minutes from the event. If the event code does not indicate whether the minutes is before or after the event, then the offset is assumed to be after the event." - } - } - } - ] - }, - "ContactDetail": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of an individual to contact." - }, - "telecom": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ContactPoint", - "description": "The contact details for the individual (if a name was provided) or the organization." - } - } - } - } - ] - }, - "RelatedArtifact": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The type of relationship to the related artifact." - }, - "label": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A short label that can be used to reference the citation from elsewhere in the containing artifact, such as a footnote index." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A brief description of the document or knowledge resource being referenced, suitable for display to a consumer." - }, - "citation": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A bibliographic citation for the related artifact. This text SHOULD be formatted according to an accepted citation format." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "A url for the artifact that can be followed to access the actual content." - }, - "document": { - "$ref": "#/components/schemas/Attachment", - "description": "The document being referenced, represented as an attachment. This is exclusive with the resource element." - }, - "resource": { - "type": "string", - "pattern": "\\S*", - "description": "The related resource, such as a library, value set, profile, or other knowledge resource." - } - }, - "required": [ - "type" - ] - } - ] - }, - "UsageContext": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "code": { - "$ref": "#/components/schemas/Coding", - "description": "A code that identifies the type of context being specified by this usage context." - }, - "valueCodeableConcept": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueQuantity": { - "$ref": "#/components/schemas/Quantity", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueRange": { - "$ref": "#/components/schemas/Range", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueReference": { - "$ref": "#/components/schemas/Reference", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - } - }, - "required": [ - "code" - ] - } - ] - }, - "Meta": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed." - }, - "source": { - "type": "string", - "pattern": "\\S*", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - } - ] - }, - "Narrative": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The status of the narrative - whether it's entirely generated (from just the defined data or the extensions too), or whether a human authored it and it may contain additional data." - }, - "div": { - "type": "string", - "description": "The actual narrative content, a stripped down version of XHTML." - } - }, - "required": [ - "status", - "div" - ] - } - ] - }, - "Extension": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "Source of the definition for the extension code - a logical name or a URL." - }, - "valueBase64Binary": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueBoolean": { - "type": "boolean", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCanonical": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCode": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDateTime": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDecimal": { - "type": "number", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueInstant": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueInteger": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMarkdown": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueOid": { - "type": "string", - "pattern": "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valuePositiveInt": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueTime": { - "type": "string", - "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUnsignedInt": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUri": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUrl": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUuid": { - "type": "string", - "pattern": "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAddress": { - "$ref": "#/components/schemas/Address", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAge": { - "$ref": "#/components/schemas/Age", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAnnotation": { - "$ref": "#/components/schemas/Annotation", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAttachment": { - "$ref": "#/components/schemas/Attachment", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCodeableConcept": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCoding": { - "$ref": "#/components/schemas/Coding", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueContactPoint": { - "$ref": "#/components/schemas/ContactPoint", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCount": { - "$ref": "#/components/schemas/Count", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDistance": { - "$ref": "#/components/schemas/Distance", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDuration": { - "$ref": "#/components/schemas/Duration", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueHumanName": { - "$ref": "#/components/schemas/HumanName", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueIdentifier": { - "$ref": "#/components/schemas/Identifier", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMoney": { - "$ref": "#/components/schemas/Money", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valuePeriod": { - "$ref": "#/components/schemas/Period", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueQuantity": { - "$ref": "#/components/schemas/Quantity", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRange": { - "$ref": "#/components/schemas/Range", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRatio": { - "$ref": "#/components/schemas/Ratio", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueReference": { - "$ref": "#/components/schemas/Reference", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueSampledData": { - "$ref": "#/components/schemas/SampledData", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueSignature": { - "$ref": "#/components/schemas/Signature", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueTiming": { - "$ref": "#/components/schemas/Timing", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueContactDetail": { - "$ref": "#/components/schemas/ContactDetail", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRelatedArtifact": { - "$ref": "#/components/schemas/RelatedArtifact", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUsageContext": { - "$ref": "#/components/schemas/UsageContext", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMeta": { - "$ref": "#/components/schemas/Meta", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - } - }, - "required": [ - "url" - ] - } - } - } - } \ No newline at end of file +{ + "openapi": "3.0.0", + "info": { + "title": "Immunization-fhir-api", + "version": "Computed and injected at build time by `scripts/set_version.py`", + "description": "## Overview\n \nUse this API to access a patient's immunisation record. It is part of the [Vaccinations Data Flow Management](https://digital.nhs.uk/services/vaccinations-data-flow-management). It is intended to extend and replace [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) and existing [Vaccination](https://digital.nhs.uk/developer/api-catalogue/vaccination) flows. \n\n### You can:\n \n- create and record a patient immunisation `yet to be released`\n- search for a patient's immunisation records\n- get the details of an immunisation record\n- update an immunisation record `yet to be released`\n- identify an immunisation record as entered in error `yet to be released`\n \n### You cannot use this API to:\n \n- retrieve the immunisation record of multiple patients at once\n- record or update a patient's demographic details\n> To get demographic details from the [Personal Demographics Service](https://digital.nhs.uk/services/personal-demographics-service), use the Personal Demographics FHIR API.\n \nYou can create, read, update and delete:\n \n- Vaccination events for the disease types found in this list {To be confirm}\n \n### Data availability, timing and quality\n\nThis is a real time service, constrained by the time taken for providers to transfer vaccination events. In most cases the latest a record will be available is within 48 hours of the immunisation event.\n\nThe API search interaction will only return immunisation records based on a traced NHS number. Other interactions require the immunisation ID assigned by the API to interact with individual records for read, update and delete.\n\nThe vaccination events for all disease types are limited to vaccinations administered on behalf of NHS England.\n\nThere is a limited scope of data validation upon receipt of the data. Whilst the data is generally of a good, reliable quality, consumers must be aware data is shared as received and users should consider the risk of potential absences or inaccuracies of the data. \n\n \n## Who can use this API \nThis API can only be used where there is a commercial, legal and clinical basis to do so. Make sure you have a valid use case before you go too far with your development.\n\nYou must demonstrate you have a valid use case as part of digital onboarding. As this service is disease type agnostic, there is also a shortened onboarding process for each disease type, to explain the legal, commercial and clinical justifications. \n\nYou must do this before you can go live refer to the [Onboarding](https://digital.nhs.uk/developer/api-catalogue/immunisation-fhir-api#overview--onboarding).\n\n## Who can access immunisation event records\n \nHealth and care organisations in England can access immunisation records.\n\nLegitimate direct care examples include NHS organisations delivering healthcare, local authorities delivering care, third sector and private sector health and care organisations, and developers delivering systems to health and care organisations.\n\nA future capability will be to enable patients who receive health and social care or make use of NHS services in England to access their vaccination records.\n\nThe immunisation event records captured via the API are used for a number of purposes including (some uses include distribution of the data by means other than this API)\n \n- supporting a patient getting a vaccination\n- supporting patient managing their immunisation health\n- identifying those to be invited to vaccination nationally\n- identifying those to be invited to vaccination locally\n- supporting the booking of a vaccination appointment\n- supporting the payment reconciliation process on behalf of NHS England / NHS Business Services Authority\n- supporting measuring the overall success of a vaccination campaign to help inform the success of the [NHS Vaccination Strategy](https://www.england.nhs.uk/long-read/nhs-vaccination-strategy/)\n\n## API status and roadmap\nThis API is [in development](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#statuses).\n \nTo see our roadmap, or to suggest, comment or vote on features for this API, see our [interactive product backlog](https://nhs-digital-api-management.featureupvote.com/?tag=immunisation).\n \nIf you have any other queries, [contact us](https://digital.nhs.uk/developer/help-and-support).\n \n## Service level\nThis API will be a platinum service, meaning it is operational and supported 24 x 7 x 365.\n \nFor more details, see [service levels](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#service-levels).\n \n## Technology\n \nThis API is [RESTful](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#basic-rest).\n \nIt conforms to the [FHIR](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) global standard for health care data exchange, specifically to [FHIR R4 (v4.0.1)](https://hl7.org/fhir/r4/), except that it does not support the [capabilities](http://hl7.org/fhir/R4/http.html#capabilities) interaction.\n \nIt includes some country-specific FHIR extensions, which conform to [FHIR UK Core](https://digital.nhs.uk/services/fhir-uk-core), specifically [fhir.r4.ukcore.stu2](https://simplifier.net/packages/fhir.r4.ukcore.stu2).\n \nYou do not need to know much about FHIR to use this API - FHIR APIs are just RESTful APIs that follow specific rules.\nIn particular:\n- resource names are capitalised and singular, and use US spellings, for example `/Immunization` not `/immunisations`\n- array names are singular, for example `entry` not `entries` for address lines\n- data items that are country-specific and thus not included in the FHIR global base resources are usually wrapped in an `extension` object\n \nThere are [libraries and SDKs available](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) to help with FHIR API integration.\n \n## Network access\nThis API is available on the internet and, indirectly, on the [Health and Social Care Network (HSCN)](https://digital.nhs.uk/services/health-and-social-care-network).\n \nFor more details see [Network access for APIs](https://digital.nhs.uk/developer/guides-and-documentation/network-access-for-apis).\n \n## Security and authorisation\n \nThis API has a single access mode:\n- application-restricted access\n\nIn the future we intend to offer user-restricted access. \n \n### Application-restricted access\n \nThis access mode is [application-restricted](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation#application-restricted-apis), meaning we authenticate the calling application but not the end user.\n \nTo use this access mode, use the following security pattern:\n- [Application-restricted RESTful API - signed JWT authentication](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication)\n\nAccess may be restricted to certain functionality based on your user need\n \ncontact:\n \n name: Immunization-fhir-api API Support\n \n url: 'https://digital.nhs.uk/developer/help-and-support'\n \n email: api.management@nhs.net\n\n## Errors\nWe use standard HTTP status codes to show whether an API request succeeded or not. They are usually in the range:\n\n* 200 to 299 if it succeeded, including code 202 if it was accepted by an API that needs to wait for further action\n* 400 to 499 if it failed because of a client error by your application\n* 500 to 599 if it failed because of an error on our server\n\nErrors specific to each API are shown in the Endpoints section, under Response. See our [reference guide](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#http-status-codes) for more on errors.\n\n## Open source\n\nYou might find the following [open source](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#open-source) resources useful:\n\n| Resource | Description | Links |\n|---------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------|\n| Immunisation FHIR API | Source code for the API proxy, sandbox and specification. | [GitHub repo](https://github.dev/NHSDigital/immunisation-fhir-api/) |\n| FHIR libraries and SDKs | Various open source libraries for integrating with FHIR APIs. | [FHIR libraries and SDKs](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) |\n| nhs-number | Python package containing utilities for NHS numbers including validity checks, normalisation and generation. | [GitHub repo](https://github.com/uk-fci/nhs-number) \\| [Python Package index](https://pypi.org/project/nhs-number/) \\| [Docs](https://nhs-number.uk-fci.tech/) |\n\nWe currently don't have any open source client libraries or sample code for this API. If you think this would be useful, you can [upvote the suggestion on our Interactive Product Backlog](https://nhs-digital-api-management.featureupvote.com/suggestions/107439/client-libraries-and-reference-implementations).\n\nThe source code for the Immunisation FHIR API is not currently in the open. If you think this would be useful, you can [upvote the suggestion on our Interactive Product Backlog](https://nhs-digital-api-management.featureupvote.com/suggestions/466692/open-source-core-spine-including-pds-eps-scr-and-more).\n\n\n## Environments and Testing\n| Environment | Base URL |\n| ----------------- | --------------------------------------------------------------------- |\n| Sandbox | `https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n| Integration | `https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` | \n| Production | `https://api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n\n### Sandbox testing\nOur [sandbox environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#sandbox-testing):\n* is for early developer testing\n* only covers a limited set of scenarios\n* is stateless, so does not actually persist any updates\n* is open access, so does not allow you to test authorisation\n\nFor details of sandbox test scenarios, or to try out the sandbox using our 'Try this API' feature, see the documentation for each endpoint.\n\n### Integration testing\nOur [integration test environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing):\n* is for formal integration testing\n* is stateful, so persists updates\n* includes authorisation, with options for user-restricted access (with or without [smartcards](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/nhs-smartcards-for-developers)) and application-restricted access \n\nFor read-only testing, we will provide an Immunisation records test pack soon.\n\nTo test creating, updating and deleting patient vaccination events, you must set up your own test data.\n\nFor more details see [integration testing with our RESTful APIs](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing-with-our-restful-apis).\n\n## Onboarding\n \nYou need to get your software approved by us before it can go live with this API.\nWe call this onboarding.\nThe onboarding process can sometimes be quite long, so it is worth planning well ahead.\n\nWhilst this API is in Alpha it is not possible to onboard to this API.\nAs part of this process, you need to demonstrate that you can manage risks and that your software conforms technically with the requirements for this API.\nInformation on this page might impact the design of your software.\n\nTo understand how our online digital onboarding process works, see [digital onboarding](https://digital.nhs.uk/developer/guides-and-documentation/digital-onboarding#using-the-digital-onboarding-portal).\n" + }, + "servers": [ + { + "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api", + "description": "Sandbox Server" + }, + { + "url": "https://int.api.service.nhs.uk/immunisation-fhir-api", + "description": "Integration Server" + } + ], + "paths": { + "/Immunization": { + "post": { + "summary": "Record a vaccination given to a patient - yet to be released", + "operationId": "createImmunization", + "description": "## Overview\nUse this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the 'disease types' enabled in this interaction and represented by the correct SNOMED concept(s) for that 'disease type'. See another page for details and disease types and coding. \nYou must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. \n\n## Sandbox testing \n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + } + ], + "requestBody": { + "type": "object", + "$ref": "#/components/requestBodies/Immunization" + }, + "responses": { + "201": { + "description": "Create Immunization operation successful", + "headers": { + "Location": { + "$ref": "#/components/headers/Location" + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms" + } + } + }, + "get": { + "summary": "Search (GET) for a patient's immunisation records", + "operationId": "searchViaGetImmunization", + "description": "## Overview\nUse this interaction to search for a patient's vaccination records using their NHS number and DiseaseType. You can request the patient's vaccination history for one or more specified 'disease types'. You may limit the vaccination records by specifying date criteria, for example if you only need to know about vaccinations administered in the last 12 months. \nVaccination event details may be obfuscated for sensitive information. The response will not include contained resources for patient or practitioner within each immunization resource it returns. A single, separate patient resource will be included in the bundle and referenced by each immunization. \nVaccination events submitted without an NHS Number will not be available for retrieval via this interaction. Also, where a patient has a change of NHS Number some or all records may be unavailable via this interaction for a short period of time whilst records are updated. \nYou must be authorised for the search interaction and the disease type(s) specified in your search in order to access the records. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\\|9000000009` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `patient.identifier` or `-immunization.target` | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/PatientIdentifier" + }, + { + "$ref": "#/components/parameters/ImmunizationTarget" + }, + { + "$ref": "#/components/parameters/DateFrom" + }, + { + "$ref": "#/components/parameters/DateTo" + }, + { + "$ref": "#/components/parameters/Include" + } + ], + "responses": { + "200": { + "description": "Search immunisation operation successful", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/Bundle" + } + } + } + }, + "400": { + "$ref": "#/components/responses/4XX-imms" + } + } + } + }, + "/Immunization/_search": { + "post": { + "summary": "Search (POST) for a patient's immunisation records", + "operationId": "searchViaPOSTImmunization", + "description": "## Overview\nYou may use this interaction as an alternative to a search with the GET verb. A POST search allows you to supply some or all parameters in the body of the request should you need to do so. It offers the same search functionality, see Search (GET) interaction for details.\n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\\|9000000009` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `patient.identifier` or -immunization.target or _include | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/PatientIdentifier" + }, + { + "$ref": "#/components/parameters/ImmunizationTarget" + }, + { + "$ref": "#/components/parameters/DateFrom" + }, + { + "$ref": "#/components/parameters/DateTo" + }, + { + "$ref": "#/components/parameters/Include" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/SearchImmunization" + }, + "responses": { + "200": { + "description": "Search immunisation operation successful", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/Bundle" + } + } + } + }, + "400": { + "$ref": "#/components/responses/4XX-imms" + } + } + } + }, + "/Immunization/{id}": { + "get": { + "summary": "Retrieve a record of an immunisation by its unique identifier", + "operationId": "readImmunization", + "description": "## Overview\nThis interaction allows you to retrieve the record of a single vaccination by our assigned id. We will return the full immunization resource as submitted, except vaccination event details may be obfuscated for sensitive information. \nThe response will include an eTag for the version of the record which has been returned. If you intend to update a record, it is recommended that you use this interaction to obtain the latest version (and eTag for the version). \nTo retrieve a full vaccination history for a patient, see the search interaction. \nYou must be authorised for the read interaction and the disease type associated with the vaccination event in order to access the record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation record found | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass Required fields `id` | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "description": "Read Immunization operation successful", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/Immunization" + } + } + } + }, + "400": { + "$ref": "#/components/responses/4XX-imms", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "3d64df5a-b753-49ec-b3df-f45c157941eb", + "issue": [ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID" + } + ] + }, + "diagnostics": "The provided event ID is either missing or not in the expected format." + } + ] + } + } + } + } + } + }, + "put": { + "summary": "Update a record of vaccination - `yet to be released`", + "operationId": "updateImmunization", + "description": "## Overview\nThis record allows you to add a new updated record of a vaccination event. Update replaces the full immunization resource, so you must provide all data fields, not just the change (Patch is not currently supported). You may obtain all the current data for the vaccination event using the read interaction, which will also return an eTag for the version. \nYou may use update to re-instate a deleted record, but our update interaction does not support creating a new vaccination event where one does not currently exist. \nYou must not change the identifier when updating a vaccination event. The identifier is used as a primary identifier by downstream systems. \nYou must be authorised for update interaction and the disease type associated with the vaccination event in order to update the record. \n\n## Sandbox testing \n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "type": "object", + "$ref": "#/components/requestBodies/Immunization" + }, + "responses": { + "200": { + "description": "Update Immunization operation successful", + "headers": { + "Location": { + "$ref": "#/components/headers/Location" + } + } + }, + "201": { + "description": "Create Immunization operation successful (requires 'updateCreateEnabled' configuration option)", + "headers": { + "Location": { + "$ref": "#/components/headers/Location" + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms" + } + } + }, + "delete": { + "summary": "Mark a record of vaccination as being entered in error - `yet to be released`", + "operationId": "deleteImmunization", + "description": "## Overview\nThis endpoint allows you to mark a record that has been entered in error.\nDeleted records will continue to be stored for a period of time but are not returned in response to read or search requests.\nA deleted record can be re-instated using the update interaction if it was incorrectly deleted. \n\n## Sandbox testing\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "204": { + "description": "Delete Immunization operation successful", + "headers": { + "Location": { + "$ref": "#/components/headers/Location" + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms" + } + } + } + } + }, + "components": { + "responses": { + "4XX-imms": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | INVALID | The provided event ID is either missing or not in the expected format. | {\"resourceType\": \"OperationOutcome\", \"id\": \"6f4ca309-19d7-4f61-90b3-acbd1f2eb8f8\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 400 | BAD_REQUEST | Search could not be processed or failed basic FHIR validation rules | {\"resourceType\": \"OperationOutcome\", \"id\": \"4ff75db4-6e7d-411d-a490-c55c11f83043\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"patient.identifier must be in the format of \\\"https://fhir.nhs.uk/Id/nhs-number|{NHS number}\\\" e.g. \\\"https://fhir.nhs.uk/Id/nhs-number|9000000009\\\"\"}]} |\n| 401 | UNAUTHORISED | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | UNAUTHORISED | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 404 | NOT_FOUND | The requested resource was not found. | {\"resourceType\": \"OperationOutcome\", \"id\": \"bc2c3c82-4392-4314-9d6b-a7345f82d923\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"The requested resource was not found.\"}]} |\n| 405 | NOT_ALLOWED | The requested method is not allowed |\n| 422 | UNPROCESSABLE_ENTITY | The proposed resource violated applicable FHIR profiles or server business rules. This should be accompanied by an OperationOutcome resource providing additional detail |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "3d64df5a-b753-49ec-b3df-f45c157941eb", + "issue": [ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "INVALID" + } + ] + }, + "diagnostics": "Search parameter patient.identifier/-immunization.target must have one value." + } + ] + } + } + } + } + }, + "requestBodies": { + "Immunization": { + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/Immunization" + } + } + }, + "required": true + }, + "SearchImmunization": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "patient.identifier": { + "type": "string", + "description": "The patient's NHS number.\nExpressed as `` where`` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", + "example": "9000000009" + }, + "-immunization.target": { + "type": "string", + "description": "Specific procedures, disorders, diseases, infections or organisms.\n", + "enum": ["COVID19", "FLU", "RSV"] + }, + "-date.from": { + "type": "string", + "format": "date", + "description": "The earliest date to be included (e.g. 2020-01-01)", + "default": "1900-01-01" + }, + "-date.to": { + "type": "string", + "format": "date", + "description": "The latest date to be included (e.g. 2020-12-31)", + "default": "9999-12-31" + }, + "_include": { + "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", + "type": "string", + "default": "Immunization:patient" + } + } + } + } + } + } + }, + "parameters": { + "Id": { + "in": "path", + "name": "id", + "required": true, + "description": "A required ID which you can use to identify an Immunization event object.\n\nMirrored back in a response header.\n", + "schema": { + "type": "string", + "example": "29dc4e84-7e72-11ee-b962-0242ac120002" + } + }, + "CorrelationID": { + "in": "header", + "name": "X-Correlation-ID", + "required": false, + "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "RequestID": { + "in": "header", + "name": "X-Request-ID", + "required": false, + "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "PatientIdentifier": { + "in": "query", + "name": "patient.identifier", + "description": "The patient's NHS number.\nExpressed as `|` where `` must be `https://fhir.nhs.uk/Id/nhs-number` and `` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", + "required": true, + "schema": { + "type": "string", + "example": "https://fhir.nhs.uk/Id/nhs-number|9000000009" + } + }, + "ImmunizationTarget": { + "in": "query", + "name": "-immunization.target", + "description": "Specific procedures, disorders, diseases, infections or organisms.\n", + "required": true, + "schema": { + "type": "string", + "enum": ["COVID19", "FLU", "RSV"] + } + }, + "DateFrom": { + "in": "query", + "name": "-date.from", + "description": "The earliest date to be included (e.g. 2020-01-01)", + "schema": { + "type": "string", + "format": "date", + "default": "1900-01-01" + } + }, + "DateTo": { + "in": "query", + "name": "-date.to", + "description": "The latest date to be included (e.g. 2020-12-31)", + "schema": { + "type": "string", + "format": "date", + "default": "9999-12-31" + } + }, + "Include": { + "in": "query", + "name": "_include", + "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", + "required": false, + "schema": { + "type": "string", + "default": "Immunization:patient" + } + } + }, + "schemas": { + "Resource": { + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "enum": [ + "Resource", + "DomainResource", + "Account", + "ActivityDefinition", + "AdverseEvent", + "AllergyIntolerance", + "Appointment", + "AppointmentResponse", + "AuditEvent", + "Basic", + "Binary", + "BiologicallyDerivedProduct", + "BodyStructure", + "Bundle", + "CapabilityStatement", + "CarePlan", + "CareTeam", + "CatalogEntry", + "ChargeItem", + "ChargeItemDefinition", + "Claim", + "ClaimResponse", + "ClinicalImpression", + "CodeSystem", + "Communication", + "CommunicationRequest", + "CompartmentDefinition", + "Composition", + "ConceptMap", + "Condition", + "Consent", + "Contract", + "Coverage", + "CoverageEligibilityRequest", + "CoverageEligibilityResponse", + "DetectedIssue", + "Device", + "DeviceDefinition", + "DeviceMetric", + "DeviceRequest", + "DeviceUseStatement", + "DiagnosticReport", + "DocumentManifest", + "DocumentReference", + "EffectEvidenceSynthesis", + "Encounter", + "Endpoint", + "EnrollmentRequest", + "EnrollmentResponse", + "EpisodeOfCare", + "EventDefinition", + "Evidence", + "EvidenceVariable", + "ExampleScenario", + "ExplanationOfBenefit", + "FamilyMemberHistory", + "Flag", + "Goal", + "GraphDefinition", + "Group", + "GuidanceResponse", + "HealthcareService", + "ImagingStudy", + "Immunization", + "ImmunizationEvaluation", + "ImmunizationRecommendation", + "ImplementationGuide", + "InsurancePlan", + "Invoice", + "Library", + "Linkage", + "List", + "Location", + "Measure", + "MeasureReport", + "Media", + "Medication", + "MedicationAdministration", + "MedicationDispense", + "MedicationKnowledge", + "MedicationRequest", + "MedicationStatement", + "MedicinalProduct", + "MedicinalProductAuthorization", + "MedicinalProductContraindication", + "MedicinalProductIndication", + "MedicinalProductIngredient", + "MedicinalProductInteraction", + "MedicinalProductManufactured", + "MedicinalProductPackaged", + "MedicinalProductPharmaceutical", + "MedicinalProductUndesirableEffect", + "MessageDefinition", + "MessageHeader", + "MolecularSequence", + "NamingSystem", + "NutritionOrder", + "Observation", + "ObservationDefinition", + "OperationDefinition", + "OperationOutcome", + "Organization", + "OrganizationAffiliation", + "Parameters", + "Patient", + "PaymentNotice", + "PaymentReconciliation", + "Person", + "PlanDefinition", + "Practitioner", + "PractitionerRole", + "Procedure", + "Provenance", + "Questionnaire", + "QuestionnaireResponse", + "RelatedPerson", + "RequestGroup", + "ResearchDefinition", + "ResearchElementDefinition", + "ResearchStudy", + "ResearchSubject", + "RiskAssessment", + "RiskEvidenceSynthesis", + "Schedule", + "SearchParameter", + "ServiceRequest", + "Slot", + "Specimen", + "SpecimenDefinition", + "StructureDefinition", + "StructureMap", + "Subscription", + "Substance", + "SubstanceNucleicAcid", + "SubstancePolymer", + "SubstanceProtein", + "SubstanceReferenceInformation", + "SubstanceSourceMaterial", + "SubstanceSpecification", + "SupplyDelivery", + "SupplyRequest", + "Task", + "TerminologyCapabilities", + "TestReport", + "TestScript", + "ValueSet", + "VerificationResult", + "VisionPrescription" + ] + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + } + }, + "required": ["resourceType"] + }, + "Immunization": { + "type": "object", + "required": ["status", "vaccineCode", "patient"], + "properties": { + "text": { + "$ref": "#/components/schemas/Narrative", + "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." + }, + "contained": { + "type": "array", + "items": { + "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "example": "Immunization" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + } + }, + "required": ["resourceType"] + } + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + }, + "identifier": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Identifier", + "description": "A unique identifier assigned to this immunization record." + } + }, + "status": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Indicates the current status of the immunization event." + }, + "statusReason": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates the reason the immunization event was not performed." + }, + "vaccineCode": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Vaccine that was administered or was to be administered." + }, + "patient": { + "$ref": "#/components/schemas/Reference", + "description": "The patient who either received or did not receive the immunization." + }, + "encounter": { + "$ref": "#/components/schemas/Reference", + "description": "The visit or admission or other contact between patient and health care provider the immunization was performed as part of." + }, + "occurrenceDateTime": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date vaccine administered or was to be administered." + }, + "occurrenceString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Date vaccine administered or was to be administered." + }, + "recorded": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event." + }, + "primarySource": { + "type": "boolean", + "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded." + }, + "reportOrigin": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The source of the data when the report of the immunization event is not based on information from the person who administered the vaccine." + }, + "location": { + "$ref": "#/components/schemas/Reference", + "description": "The service delivery location where the vaccine administration occurred." + }, + "manufacturer": { + "$ref": "#/components/schemas/Reference", + "description": "Name of vaccine manufacturer." + }, + "lotNumber": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Lot number of the vaccine product." + }, + "expirationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", + "description": "Date vaccine batch expires." + }, + "site": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Body site where vaccine was administered." + }, + "route": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The path by which the vaccine product is taken into the body." + }, + "doseQuantity": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The quantity of vaccine product that was administered." + }, + "performer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Performer", + "description": "Indicates who performed the immunization event." + } + }, + "note": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Annotation", + "description": "Extra information about the immunization that is not conveyed by the other attributes." + } + }, + "reasonCode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Reasons why the vaccine was administered." + } + }, + "reasonReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference", + "description": "Condition, Observation or DiagnosticReport that supports why the immunization was administered." + } + }, + "isSubpotent": { + "type": "boolean", + "description": "Indication if a dose is considered to be subpotent. By default, a dose should be considered to be potent." + }, + "subpotentReason": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Reason why a dose is considered to be subpotent." + } + }, + "education": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Education", + "description": "Educational material presented to the patient (or guardian) at the time of vaccine administration." + } + }, + "programEligibility": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates a patient's eligibility for a funding program." + } + }, + "fundingSource": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates the source of the vaccine actually administered. This may be different than the patient eligibility (e.g. the patient may be eligible for a publically purchased vaccine but due to inventory issues, vaccine purchased with private funds was actually administered)." + }, + "reaction": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Reaction", + "description": "Categorical data indicating that an adverse event is associated in time to an immunization." + } + }, + "protocolApplied": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_ProtocolApplied", + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose." + } + } + }, + "example": { + "resourceType": "Immunization", + "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Owl", + "given": ["Barney"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449310475" + } + ], + "name": [ + { + "family": "Owler", + "given": ["Ozzie"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "ZZ99 3CZ" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "urn:iso:std:iso:3166", + "value": "GB" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "N2N9I" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + } + }, + "Bundle": { + "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", + "type": "object", + "required": ["resourceType", "type"], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Bundle`.", + "type": "string", + "example": "Bundle" + }, + "type": { + "description": "Indicates how the bundle is intended to be used. Always `searchset`.", + "type": "string", + "example": "searchset" + }, + "link": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Link", + "description": "A series of links that provide context to this bundle." + } + }, + "entry": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Entry", + "description": "An entry in a bundle resource - will either contain a resource or information about a resource (transactions and history only)." + } + }, + "total": { + "type": "integer", + "format": "int32", + "description": "If a set of search matches, this is the total number of entries of type 'match' across all pages in the search. It does not include search.mode = 'include' or 'outcome' entries and it does not provide a count of the number of entries in the Bundle." + } + }, + "example": { + "resourceType": "Bundle", + "type": "searchset", + "link": [ + { + "relation": "self", + "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=RSV&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9000000009" + } + ], + "entry": [ + { + "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", + "resource": { + "resourceType": "Immunization", + "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "identifier": [ + { + "use": "official", + "system": "https://supplierABC/identifiers/vacc", + "value": "e2154d29-1ead-4830-a513-0d59705078fa" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449306206" + } + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "urn:iso:std:iso:3166", + "value": "GB" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + }, + "display": "UNIVERSITY HOSPITAL OF WALES" + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005", + "display": "Disease outbreak (event)" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "resource": { + "resourceType": "Patient", + "id": "9449306206", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449306206" + } + ], + "birthDate": "2014-03-25" + }, + "search": { + "mode": "include" + } + } + ], + "total": 1 + } + }, + "OperationOutcome": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/Narrative", + "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." + }, + "contained": { + "type": "array", + "items": { + "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "example": "OperationOutcome" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "issue": { + "type": "array", + "items": { + "type": "object", + "properties": { + "severity": { + "type": "string" + }, + "code": { + "type": "string" + }, + "details": { + "type": "object", + "properties": { + "coding": { + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + }, + "diagnostics": { + "type": "string" + } + } + } + } + }, + "required": ["resourceType"] + } + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + }, + "issue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OperationOutcome_Issue", + "description": "An error, warning, or information message that results from a system action." + }, + "minItems": 1 + } + }, + "required": ["issue"], + "example": { + "resourceType": "OperationOutcome", + "meta": { + "versionId": "BnpJOa5-Sb", + "lastUpdated": "2021-04-12T14:34:36.061-05:00", + "source": "BCL3d5NERb", + "profile": ["xSempdez3Y"], + "security": [ + { + "system": "tczS7uP8XL", + "version": "IXKbCw05qO", + "code": "NvDP1hL64Y", + "display": "_r1z5oJld1", + "userSelected": true + } + ], + "tag": [ + { + "system": "2qqXHsE1Mx", + "version": "lybFyQ1tBj", + "code": "Q9w075fYd3", + "display": "Nm2QqbYibP", + "userSelected": true + }, + { + "code": "ibm/complete-mock" + } + ] + }, + "implicitRules": "l8KHk6qOt4", + "language": "en-US", + "text": { + "status": "additional", + "div": "
" + }, + "issue": [ + { + "severity": "warning", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "eQgFofRHmJ", + "version": "T524HDk5Za", + "code": "tC_7iQg31j", + "display": "s0bLc4W5KE", + "userSelected": true + } + ], + "text": "BfBVppHmsh" + }, + "diagnostics": "EcvPDGbK0q", + "location": ["GK5ihTmfe6"], + "expression": ["Uidx_swV4Z"] + } + ] + } + }, + "Bundle_Entry": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "link": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Link", + "description": "A series of links that provide context to this entry." + } + }, + "fullUrl": { + "type": "string", + "pattern": "\\S*", + "description": "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource - i.e. if the fullUrl is not a urn:uuid, the URL shall be version-independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified." + }, + "resource": { + "description": "The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type." + }, + "resourceType": { + "type": "string", + "example": "Immunization" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + }, + "search": { + "$ref": "#/components/schemas/Bundle_Entry_Search", + "description": "Information about the search process that lead to the creation of this entry." + }, + "request": { + "$ref": "#/components/schemas/Bundle_Entry_Request", + "description": "Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry." + }, + "response": { + "$ref": "#/components/schemas/Bundle_Entry_Response", + "description": "Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history." + } + } + } + ] + }, + "Bundle_Entry_Response": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The status code returned by processing this entry. The status SHALL start with a 3 digit HTTP code (e.g. 404) and may contain the standard HTTP description associated with the status code." + }, + "location": { + "type": "string", + "pattern": "\\S*", + "description": "The location header created by processing this operation, populated if the operation returns a location." + }, + "etag": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The Etag for the resource, if the operation for the entry produced a versioned resource (see [Resource Metadata and Versioning](http.html#versioning) and [Managing Resource Contention](http.html#concurrency))." + }, + "lastModified": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "The date/time that the resource was modified on the server." + }, + "outcome": { + "$ref": "#/components/schemas/Resource", + "description": "An OperationOutcome containing hints and warnings produced as part of processing this entry in a batch or transaction." + } + }, + "required": ["status"] + } + ] + }, + "Bundle_Entry_Request": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "method": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "In a transaction or batch, this is the HTTP action to be executed for this entry. In a history bundle, this indicates the HTTP action that occurred." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "The URL for this entry, relative to the root (the address to which the request is posted)." + }, + "ifNoneMatch": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "If the ETag values match, return a 304 Not Modified status. See the API documentation for [\"Conditional Read\"](http.html#cread)." + }, + "ifModifiedSince": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "Only perform the operation if the last updated date matches. See the API documentation for [\"Conditional Read\"](http.html#cread)." + }, + "ifMatch": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Only perform the operation if the Etag value matches. For more information, see the API section [\"Managing Resource Contention\"](http.html#concurrency)." + }, + "ifNoneExist": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Instruct the server not to perform the create if a specified resource already exists. For further information, see the API documentation for [\"Conditional Create\"](http.html#ccreate). This is just the query portion of the URL - what follows the \"?\" (not including the \"?\")." + } + }, + "required": ["method", "url"] + } + ] + }, + "Bundle_Entry_Search": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "mode": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Why this entry is in the result set - whether it's included as a match or because of an _include requirement, or to convey information or warning information about the search process." + }, + "score": { + "type": "number", + "description": "When searching, the server's search ranking score for the entry." + } + } + } + ] + }, + "Bundle_Link": { + "allOf": [ + { + "type": "object", + "properties": { + "relation": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1)." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "The reference details for the link." + } + }, + "required": ["relation", "url"] + } + ] + }, + "Immunization_ProtocolApplied": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "series": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "One possible path to achieve presumed immunity against a disease - within the context of an authority." + }, + "authority": { + "$ref": "#/components/schemas/Reference", + "description": "Indicates the authority who published the protocol (e.g. ACIP) that is being followed." + }, + "targetDisease": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The vaccine preventable disease the dose is being administered against." + } + }, + "doseNumberPositiveInt": { + "type": "integer", + "format": "int32", + "description": "Nominal position in a series." + }, + "doseNumberString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Nominal position in a series." + }, + "seriesDosesPositiveInt": { + "type": "integer", + "format": "int32", + "description": "The recommended number of doses to achieve immunity." + }, + "seriesDosesString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The recommended number of doses to achieve immunity." + } + } + } + ] + }, + "Immunization_Reaction": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "date": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date of reaction to the immunization." + }, + "detail": { + "$ref": "#/components/schemas/Reference", + "description": "Details of the reaction." + }, + "reported": { + "type": "boolean", + "description": "Self-reported indicator." + } + } + } + ] + }, + "Immunization_Education": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "documentType": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Identifier of the material presented to the patient." + }, + "reference": { + "type": "string", + "pattern": "\\S*", + "description": "Reference pointer to the educational material given to the patient if the information was on line." + }, + "publicationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date the educational material was published." + }, + "presentationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date the educational material was given to the patient." + } + } + } + ] + }, + "Immunization_Performer": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "function": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Describes the type of performance (e.g. ordering provider, administering provider, etc.)." + }, + "actor": { + "$ref": "#/components/schemas/Reference", + "description": "The practitioner or organization who performed the action." + } + }, + "required": ["actor"] + } + ] + }, + "OperationOutcome_Issue": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "severity": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Indicates whether the issue indicates a variation from successful processing." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." + }, + "details": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Additional details about the error. This may be a text description of the error or a system code that identifies the error." + }, + "diagnostics": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Additional diagnostic information about the issue." + }, + "location": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "This element is deprecated because it is XML specific. It is replaced by issue.expression, which is format independent, and simpler to parse. \n\nFor resource issues, this will be a simple XPath limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised. For HTTP errors, will be \"http.\" + the parameter name." + } + }, + "expression": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A [simple subset of FHIRPath](fhirpath.html#simple) limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised." + } + } + }, + "required": ["severity", "code"] + } + ] + }, + "Element": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + }, + "example": [ + { + "url": "http://example.com", + "valueString": "text value" + } + ] + } + } + }, + "BackboneElement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + } + } + }, + "Address": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The purpose of this address." + }, + "type": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Distinguishes between physical addresses (those you can visit) and mailing addresses (e.g. PO Boxes and care-of addresses). Most addresses are both." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Specifies the entire address as it should be displayed e.g. on a postal label. This may be provided instead of or as well as the specific parts." + }, + "line": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "This component contains the house number, apartment number, street name, street direction, P.O. Box number, delivery hints, and similar address information." + } + }, + "city": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of the city, town, suburb, village or other community or delivery center." + }, + "district": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of the administrative area (county)." + }, + "state": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Sub-unit of a country with limited sovereignty in a federally organized country. A code may be used if codes are in common use (e.g. US 2 letter state codes)." + }, + "postalCode": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A postal code designating a region defined by the postal service." + }, + "country": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Country - a nation as commonly understood or generally accepted." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period when address was/is in use." + } + } + } + ] + }, + "Age": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Annotation": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "authorReference": { + "$ref": "#/components/schemas/Reference", + "description": "The individual responsible for making the annotation." + }, + "authorString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The individual responsible for making the annotation." + }, + "time": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Indicates when this particular annotation was made." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The text of the annotation in markdown format." + } + }, + "required": ["text"] + } + ] + }, + "Attachment": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The human language of the content. The value can be any valid value according to BCP 47." + }, + "data": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The actual data of the attachment - a sequence of bytes, base64 encoded." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "A location where the data can be accessed." + }, + "size": { + "type": "integer", + "format": "int32", + "description": "The number of bytes of data that make up this attachment (before base64 encoding, if that is done)." + }, + "hash": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The calculated hash of the data using SHA-1. Represented using base64." + }, + "title": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A label or set of text to display in place of the data." + }, + "creation": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The date that the attachment was first created." + } + } + } + ] + }, + "CodeableConcept": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "coding": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "A reference to a code defined by a terminology system." + } + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user." + } + } + }, + "Coding": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "system": { + "type": "string", + "pattern": "\\S*", + "description": "The identification of the code system that defines the meaning of the symbol in the code." + }, + "version": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination)." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A representation of the meaning of the code in the system, following the rules of the system." + }, + "userSelected": { + "type": "boolean", + "description": "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays)." + } + } + }, + "ContactPoint": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "system": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Telecommunications form for contact point - what communications system is required to make use of the contact." + }, + "value": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The actual contact point details, in a form that is meaningful to the designated communication system (i.e. phone number or email address)." + }, + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the purpose for the contact point." + }, + "rank": { + "type": "integer", + "format": "int32", + "description": "Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period when the contact point was/is in use." + } + } + } + ] + }, + "Count": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Distance": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Duration": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "HumanName": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the purpose for this name." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts." + }, + "family": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father." + }, + "given": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Given name." + } + }, + "prefix": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name." + } + }, + "suffix": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name." + } + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Indicates the period of time when this name was valid for the named person." + } + } + } + ] + }, + "Identifier": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The purpose of this identifier." + }, + "type": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A coded type for the identifier that can be used to determine which identifier to use for a specific purpose." + }, + "system": { + "type": "string", + "pattern": "\\S*", + "description": "Establishes the namespace for the value - that is, a URL that describes a set values that are unique." + }, + "value": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The portion of the identifier typically relevant to the user and which is unique within the context of the system." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period during which identifier is/was valid for use." + }, + "assigner": { + "$ref": "#/components/schemas/Reference", + "description": "Organization that issued/manages the identifier.", + "example": { + "reference": "Organization/123", + "type": "Organization", + "display": "The Assigning Organization" + } + } + } + }, + "Money": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "value": { + "type": "number", + "description": "Numerical value (with implicit precision)." + }, + "currency": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "ISO 4217 Currency Code." + } + } + } + ] + }, + "MoneyQuantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Period": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "start": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The start of the period. The boundary is inclusive." + }, + "end": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time." + } + } + } + ] + }, + "Quantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "value": { + "type": "number", + "description": "The value of the measured amount. The value includes an implicit precision in the presentation of the value." + }, + "comparator": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "How the value should be understood and represented - whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is \"<\" , then the real value is < stated value." + }, + "unit": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A human-readable form of the unit." + }, + "system": { + "type": "string", + "pattern": "\\S*", + "description": "The identification of the system that provides the coded form of the unit." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A computer processable form of the unit in some unit representation system." + } + } + } + ] + }, + "Range": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "low": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The low limit. The boundary is inclusive." + }, + "high": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The high limit. The boundary is inclusive." + } + } + } + ] + }, + "Ratio": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "numerator": { + "$ref": "#/components/schemas/Quantity", + "description": "The value of the numerator." + }, + "denominator": { + "$ref": "#/components/schemas/Quantity", + "description": "The value of the denominator." + } + } + } + ] + }, + "Reference": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "reference": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources." + }, + "type": { + "type": "string", + "pattern": "\\S*", + "description": "The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent.\n\nThe type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. \"Patient\" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources)." + }, + "identifier": { + "$ref": "#/components/schemas/Identifier", + "description": "An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Plain text narrative that identifies the resource in addition to the resource reference." + } + } + }, + "SampledData": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "origin": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The base quantity that a measured value of zero represents. In addition, this provides the units of the entire measurement series." + }, + "period": { + "type": "number", + "description": "The length of time between sampling times, measured in milliseconds." + }, + "factor": { + "type": "number", + "description": "A correction factor that is applied to the sampled data points before they are added to the origin." + }, + "lowerLimit": { + "type": "number", + "description": "The lower limit of detection of the measured points. This is needed if any of the data points have the value \"L\" (lower than detection limit)." + }, + "upperLimit": { + "type": "number", + "description": "The upper limit of detection of the measured points. This is needed if any of the data points have the value \"U\" (higher than detection limit)." + }, + "dimensions": { + "type": "integer", + "format": "int32", + "description": "The number of sample points at each time point. If this value is greater than one, then the dimensions will be interlaced - all the sample points for a point in time will be recorded at once." + }, + "data": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A series of data points which are decimal values separated by a single space (character u20). The special values \"E\" (error), \"L\" (below detection limit) and \"U\" (above detection limit) can also be used in place of a decimal value." + } + }, + "required": ["origin", "period", "dimensions"] + } + ] + }, + "SimpleQuantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Signature": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "type": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "An indication of the reason that the entity signed this document. This may be explicitly included as part of the signature information and can be used when determining accountability for various actions concerning the document." + }, + "minItems": 1 + }, + "when": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the digital signature was signed." + }, + "who": { + "$ref": "#/components/schemas/Reference", + "description": "A reference to an application-usable description of the identity that signed (e.g. the signature used their private key)." + }, + "onBehalfOf": { + "$ref": "#/components/schemas/Reference", + "description": "A reference to an application-usable description of the identity that is represented by the signature." + }, + "targetFormat": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A mime type that indicates the technical format of the target resources signed by the signature." + }, + "sigFormat": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A mime type that indicates the technical format of the signature. Important mime types are application/signature+xml for X ML DigSig, application/jose for JWS, and image/* for a graphical image of a signature, etc." + }, + "data": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The base64 encoding of the Signature content. When signature is not recorded electronically this element would be empty." + } + }, + "required": ["type", "when", "who"] + } + ] + }, + "Timing": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "event": { + "type": "array", + "items": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Identifies specific times when the event occurs." + } + }, + "repeat": { + "$ref": "#/components/schemas/Timing_Repeat", + "description": "A set of rules that describe when the event is scheduled." + }, + "code": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A code for the timing schedule (or just text in code.text). Some codes such as BID are ubiquitous, but many institutions define their own additional codes. If a code is provided, the code is understood to be a complete statement of whatever is specified in the structured timing data, and either the code or the data may be used to interpret the Timing, with the exception that .repeat.bounds still applies over the code (and is not contained in the code)." + } + } + } + ] + }, + "Timing_Repeat": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "boundsDuration": { + "$ref": "#/components/schemas/Duration", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "boundsRange": { + "$ref": "#/components/schemas/Range", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "boundsPeriod": { + "$ref": "#/components/schemas/Period", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "count": { + "type": "integer", + "format": "int32", + "description": "A total count of the desired number of repetitions across the duration of the entire timing specification. If countMax is present, this element indicates the lower bound of the allowed range of count values." + }, + "countMax": { + "type": "integer", + "format": "int32", + "description": "If present, indicates that the count is a range - so to perform the action between [count] and [countMax] times." + }, + "duration": { + "type": "number", + "description": "How long this thing happens for when it happens. If durationMax is present, this element indicates the lower bound of the allowed range of the duration." + }, + "durationMax": { + "type": "number", + "description": "If present, indicates that the duration is a range - so to perform the action between [duration] and [durationMax] time length." + }, + "durationUnit": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The units of time for the duration, in UCUM units." + }, + "frequency": { + "type": "integer", + "format": "int32", + "description": "The number of times to repeat the action within the specified period. If frequencyMax is present, this element indicates the lower bound of the allowed range of the frequency." + }, + "frequencyMax": { + "type": "integer", + "format": "int32", + "description": "If present, indicates that the frequency is a range - so to repeat between [frequency] and [frequencyMax] times within the period or period range." + }, + "period": { + "type": "number", + "description": "Indicates the duration of time over which repetitions are to occur; e.g. to express \"3 times per day\", 3 would be the frequency and \"1 day\" would be the period. If periodMax is present, this element indicates the lower bound of the allowed range of the period length." + }, + "periodMax": { + "type": "number", + "description": "If present, indicates that the period is a range from [period] to [periodMax], allowing expressing concepts such as \"do this once every 3-5 days." + }, + "periodUnit": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The units of time for the period in UCUM units." + }, + "dayOfWeek": { + "type": "array", + "items": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "If one or more days of week is provided, then the action happens only on the specified day(s)." + } + }, + "timeOfDay": { + "type": "array", + "items": { + "type": "string", + "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", + "description": "Specified time of day for action to take place." + } + }, + "when": { + "type": "array", + "items": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "An approximate time period during the day, potentially linked to an event of daily living that indicates when the action should occur." + } + }, + "offset": { + "type": "integer", + "format": "int32", + "description": "The number of minutes from the event. If the event code does not indicate whether the minutes is before or after the event, then the offset is assumed to be after the event." + } + } + } + ] + }, + "ContactDetail": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of an individual to contact." + }, + "telecom": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContactPoint", + "description": "The contact details for the individual (if a name was provided) or the organization." + } + } + } + } + ] + }, + "RelatedArtifact": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The type of relationship to the related artifact." + }, + "label": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A short label that can be used to reference the citation from elsewhere in the containing artifact, such as a footnote index." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A brief description of the document or knowledge resource being referenced, suitable for display to a consumer." + }, + "citation": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A bibliographic citation for the related artifact. This text SHOULD be formatted according to an accepted citation format." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "A url for the artifact that can be followed to access the actual content." + }, + "document": { + "$ref": "#/components/schemas/Attachment", + "description": "The document being referenced, represented as an attachment. This is exclusive with the resource element." + }, + "resource": { + "type": "string", + "pattern": "\\S*", + "description": "The related resource, such as a library, value set, profile, or other knowledge resource." + } + }, + "required": ["type"] + } + ] + }, + "UsageContext": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/Coding", + "description": "A code that identifies the type of context being specified by this usage context." + }, + "valueCodeableConcept": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueQuantity": { + "$ref": "#/components/schemas/Quantity", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueRange": { + "$ref": "#/components/schemas/Range", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueReference": { + "$ref": "#/components/schemas/Reference", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + } + }, + "required": ["code"] + } + ] + }, + "Meta": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed." + }, + "source": { + "type": "string", + "pattern": "\\S*", + "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + } + } + ] + }, + "Narrative": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The status of the narrative - whether it's entirely generated (from just the defined data or the extensions too), or whether a human authored it and it may contain additional data." + }, + "div": { + "type": "string", + "description": "The actual narrative content, a stripped down version of XHTML." + } + }, + "required": ["status", "div"] + } + ] + }, + "Extension": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "Source of the definition for the extension code - a logical name or a URL." + }, + "valueBase64Binary": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueBoolean": { + "type": "boolean", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCanonical": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCode": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDateTime": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDecimal": { + "type": "number", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueInstant": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueInteger": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMarkdown": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueOid": { + "type": "string", + "pattern": "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valuePositiveInt": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueTime": { + "type": "string", + "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUnsignedInt": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUri": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUrl": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUuid": { + "type": "string", + "pattern": "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAddress": { + "$ref": "#/components/schemas/Address", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAge": { + "$ref": "#/components/schemas/Age", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAnnotation": { + "$ref": "#/components/schemas/Annotation", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAttachment": { + "$ref": "#/components/schemas/Attachment", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCodeableConcept": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCoding": { + "$ref": "#/components/schemas/Coding", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueContactPoint": { + "$ref": "#/components/schemas/ContactPoint", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCount": { + "$ref": "#/components/schemas/Count", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDistance": { + "$ref": "#/components/schemas/Distance", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDuration": { + "$ref": "#/components/schemas/Duration", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueHumanName": { + "$ref": "#/components/schemas/HumanName", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueIdentifier": { + "$ref": "#/components/schemas/Identifier", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMoney": { + "$ref": "#/components/schemas/Money", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valuePeriod": { + "$ref": "#/components/schemas/Period", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueQuantity": { + "$ref": "#/components/schemas/Quantity", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRange": { + "$ref": "#/components/schemas/Range", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRatio": { + "$ref": "#/components/schemas/Ratio", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueReference": { + "$ref": "#/components/schemas/Reference", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueSampledData": { + "$ref": "#/components/schemas/SampledData", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueSignature": { + "$ref": "#/components/schemas/Signature", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueTiming": { + "$ref": "#/components/schemas/Timing", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueContactDetail": { + "$ref": "#/components/schemas/ContactDetail", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRelatedArtifact": { + "$ref": "#/components/schemas/RelatedArtifact", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUsageContext": { + "$ref": "#/components/schemas/UsageContext", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMeta": { + "$ref": "#/components/schemas/Meta", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + } + }, + "required": ["url"] + } + }, + "headers": { + "Location": { + "schema": { + "type": "string", + "example": "29dc4e84-7e72-11ee-b962-0242ac120002" + }, + "description": "The location of the resource." + } + } + } +} diff --git a/scripts/calculate_version.py b/scripts/calculate_version.py index 56c8dc7a5..905d7de7c 100644 --- a/scripts/calculate_version.py +++ b/scripts/calculate_version.py @@ -33,9 +33,7 @@ def get_versionable_commits(repo): commits = [c for c in repo.iter_commits() if len(c.parents) == 1] # If there is a marker to start versioning from, use it. Else, start from the first commit - return list( - itertools.takewhile(lambda c: "+startversioning" not in c.message, commits) - ) + return list(itertools.takewhile(lambda c: "+startversioning" not in c.message, commits)) def is_status_set_command(commit): @@ -83,9 +81,7 @@ def calculate_version(base_major=1, base_minor=0, base_revision=0, base_pre="alp most_recent_message = status_sets[0].message.strip() if most_recent_message.startswith("+setstatus "): - pre = most_recent_message.split(" ")[ - 1 - ] # Take the first string after the command + pre = most_recent_message.split(" ")[1] # Take the first string after the command if most_recent_message == "+clearstatus": pre = None diff --git a/scripts/destroy_unused_workspaces.py b/scripts/destroy_unused_workspaces.py index c28b37328..7a58858d5 100644 --- a/scripts/destroy_unused_workspaces.py +++ b/scripts/destroy_unused_workspaces.py @@ -43,17 +43,13 @@ def list_pr_workspaces(prefix): list_command = "terraform workspace list" print(list_command) - return_code, stdout, stderr = execute_terraform_command( - list_command, cwd=os.getcwd() - ) + return_code, stdout, stderr = execute_terraform_command(list_command, cwd=os.getcwd()) if return_code == 0: workspaces = stdout.strip().split("\n") print(f"Workspaces from Terraform: {workspaces}") # Filter workspaces that contain "pr" and replace spaces pr_workspaces = [ - workspace.replace(" ", "").replace("*", "") - for workspace in workspaces - if prefix in workspace.lower() + workspace.replace(" ", "").replace("*", "") for workspace in workspaces if prefix in workspace.lower() ] return pr_workspaces else: @@ -63,37 +59,28 @@ def list_pr_workspaces(prefix): def destroy_workspace(workspace_name, project_name, project_short_name): command_select = f"terraform workspace select {workspace_name}" - tf_vars = ( - f"-var=project_name={project_name} " - f"-var=project_short_name={project_short_name} " - ) + tf_vars = f"-var=project_name={project_name} " f"-var=project_short_name={project_short_name} " command_destroy = f"terraform destroy {tf_vars} -auto-approve" command_delete = f"terraform workspace select default && terraform workspace delete {workspace_name}" try: # Command: terraform workspace select print(command_select) - return_code_select, stdout_select, stderr_select = execute_terraform_command( - command_select - ) + return_code_select, stdout_select, stderr_select = execute_terraform_command(command_select) if return_code_select != 0: print(f"Error executing select command for workspace {workspace_name}") return return_code_select, stdout_select, stderr_select # Command: terraform destroy print(command_destroy) - return_code_destroy, stdout_destroy, stderr_destroy = execute_terraform_command( - command_destroy - ) + return_code_destroy, stdout_destroy, stderr_destroy = execute_terraform_command(command_destroy) if return_code_destroy != 0: print(f"Error executing destroy command for workspace {workspace_name}") return return_code_destroy, stdout_destroy, stderr_destroy # Command: terraform workspace delete print(command_delete) - return_code_delete, stdout_delete, stderr_delete = execute_terraform_command( - command_delete - ) + return_code_delete, stdout_delete, stderr_delete = execute_terraform_command(command_delete) if return_code_delete != 0: print(f"Error executing delete command for workspace {workspace_name}") return return_code_delete, stdout_delete, stderr_delete @@ -109,9 +96,7 @@ def destroy_workspace_wrapper(workspace_name, project_name, project_short_name): try: # Retry the destroy command if it returns non-zero exit code for _ in range(2): # You can adjust the number of retries as needed - return_code, stdout, stderr = destroy_workspace( - workspace_name, project_name, project_short_name - ) + return_code, stdout, stderr = destroy_workspace(workspace_name, project_name, project_short_name) if return_code == 0: return workspace_name, "Destroyed successfully" else: @@ -133,9 +118,7 @@ def main(): print(f"Available Workspaces: {workspaces}") # Store results for all workspaces for workspace in workspaces: - workspace_name, result = destroy_workspace_wrapper( - workspace, project_name, project_short_name - ) + workspace_name, result = destroy_workspace_wrapper(workspace, project_name, project_short_name) results.append((workspace_name, result)) # Print results at the end diff --git a/scripts/pre-commit b/scripts/pre-commit deleted file mode 100644 index a7b83b930..000000000 --- a/scripts/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -make lint diff --git a/specification/components/examples/Immunization/completed_covid19_immunization_event.json b/specification/components/examples/Immunization/completed_covid19_immunization_event.json index b2a800080..50922f227 100644 --- a/specification/components/examples/Immunization/completed_covid19_immunization_event.json +++ b/specification/components/examples/Immunization/completed_covid19_immunization_event.json @@ -149,4 +149,4 @@ "doseNumberPositiveInt": 1 } ] -} \ No newline at end of file +} diff --git a/specification/components/examples/Immunization/completed_covid19_immunization_event_filtered_for_read.json b/specification/components/examples/Immunization/completed_covid19_immunization_event_filtered_for_read.json index caec3df5c..e81cefbcb 100644 --- a/specification/components/examples/Immunization/completed_covid19_immunization_event_filtered_for_read.json +++ b/specification/components/examples/Immunization/completed_covid19_immunization_event_filtered_for_read.json @@ -149,4 +149,4 @@ "doseNumberPositiveInt": 1 } ] -} \ No newline at end of file +} diff --git a/specification/components/examples/Immunization/completed_rsv_immunization_event.json b/specification/components/examples/Immunization/completed_rsv_immunization_event.json index 44cb4f67f..f38594007 100644 --- a/specification/components/examples/Immunization/completed_rsv_immunization_event.json +++ b/specification/components/examples/Immunization/completed_rsv_immunization_event.json @@ -149,4 +149,4 @@ "doseNumberPositiveInt": 1 } ] -} \ No newline at end of file +} diff --git a/specification/components/examples/Immunization/completed_rsv_immunization_event_filtered_for_read.json b/specification/components/examples/Immunization/completed_rsv_immunization_event_filtered_for_read.json index 44cb4f67f..f38594007 100644 --- a/specification/components/examples/Immunization/completed_rsv_immunization_event_filtered_for_read.json +++ b/specification/components/examples/Immunization/completed_rsv_immunization_event_filtered_for_read.json @@ -149,4 +149,4 @@ "doseNumberPositiveInt": 1 } ] -} \ No newline at end of file +} diff --git a/specification/components/examples/OperationOutcome/401-invalid_access_token.json b/specification/components/examples/OperationOutcome/401-invalid_access_token.json index 8f2760237..cf9e28796 100644 --- a/specification/components/examples/OperationOutcome/401-invalid_access_token.json +++ b/specification/components/examples/OperationOutcome/401-invalid_access_token.json @@ -1,24 +1,24 @@ { - "resourceType": "OperationOutcome", - "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta": { - "profile": [ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue": [ - { - "severity": "error", - "code": "expired", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", - "code": "SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] +} diff --git a/specification/components/examples/OperationOutcome/404-not_found.json b/specification/components/examples/OperationOutcome/404-not_found.json index e7083c5be..a45b68089 100644 --- a/specification/components/examples/OperationOutcome/404-not_found.json +++ b/specification/components/examples/OperationOutcome/404-not_found.json @@ -1,24 +1,24 @@ { - "resourceType": "OperationOutcome", - "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta": { - "profile": [ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue": [ - { - "severity": "error", - "code": "not-found", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", - "code": "NOT_FOUND" - } - ] - }, - "diagnostics": "The requested resource was not found." - } + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" ] - } + }, + "issue": [ + { + "severity": "error", + "code": "not-found", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "NOT_FOUND" + } + ] + }, + "diagnostics": "The requested resource was not found." + } + ] +} diff --git a/specification/components/schemas/GetResponse.yaml b/specification/components/schemas/GetResponse.yaml index 95047f159..d0e0deb40 100644 --- a/specification/components/schemas/GetResponse.yaml +++ b/specification/components/schemas/GetResponse.yaml @@ -90,7 +90,7 @@ properties: properties: system: type: string - default: 'http://snomed.info/sct' + default: "http://snomed.info/sct" code: type: string default: snomed @@ -108,7 +108,7 @@ properties: properties: system: type: string - default: 'http://snomed.info/sct' + default: "http://snomed.info/sct" code: type: string default: snomed @@ -132,7 +132,7 @@ properties: properties: system: type: string - default: 'http://snomed.info/sct' + default: "http://snomed.info/sct" code: type: string default: snomed @@ -152,7 +152,7 @@ properties: properties: system: type: string - default: 'http://snomed.info/sct' + default: "http://snomed.info/sct" code: type: string default: snomed @@ -243,7 +243,7 @@ properties: default: LA system: type: string - default: 'http://snomed.info/sct' + default: "http://snomed.info/sct" display: type: string default: left arm diff --git a/specification/components/schemas/Location.yaml b/specification/components/schemas/Location.yaml index 8ce5627d9..6bcad73c1 100644 --- a/specification/components/schemas/Location.yaml +++ b/specification/components/schemas/Location.yaml @@ -1,4 +1,4 @@ schema: type: string - example: '29dc4e84-7e72-11ee-b962-0242ac120002' -description: The location of the resource. \ No newline at end of file + example: "29dc4e84-7e72-11ee-b962-0242ac120002" +description: The location of the resource. diff --git a/specification/components/schemas/OperationOutcome.yaml b/specification/components/schemas/OperationOutcome.yaml index 1dd170b6a..e7a67489e 100644 --- a/specification/components/schemas/OperationOutcome.yaml +++ b/specification/components/schemas/OperationOutcome.yaml @@ -76,11 +76,11 @@ properties: system: type: string description: URI of the coding system specification. - example: 'https://fhir.nhs.uk/R4/CodeSystem/Spine-ErrorOrWarningCode' + example: "https://fhir.nhs.uk/R4/CodeSystem/Spine-ErrorOrWarningCode" version: type: string description: Version of the coding system in use. - example: '1' + example: "1" code: type: string description: Symbol in syntax defined by the system. diff --git a/specification/immunisation-fhir-api.json b/specification/immunisation-fhir-api.json index fea524bbb..fd65f73dd 100644 --- a/specification/immunisation-fhir-api.json +++ b/specification/immunisation-fhir-api.json @@ -1,6691 +1,6401 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Immunization-fhir-api", - "version": "Computed and injected at build time by `scripts/set_version.py`", - "description": "## Overview\n \nUse this API to access a patient's immunisation record. It is part of the [Vaccinations Data Flow Management](https://digital.nhs.uk/services/vaccinations-data-flow-management). It is intended to extend and replace [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) and existing [Vaccination](https://digital.nhs.uk/developer/api-catalogue/vaccination) flows. \n\nYou can use this API to:\n \n- create and record a patient immunisation \n- search for a patient's immunisation records\n- get the details of an immunisation record\n- update an immunisation record \n- identify an immunisation record as entered in error \n \nYou cannot use this API to:\n \n- retrieve the immunisation record of multiple patients at once \n- record or update a patient's demographic details\n \nYou can create, read, update and delete events for the following vaccination types:\n \n- coronavirus (`COVID19`)\n- influenza (`FLU`)\n- measles, mumps and rubella (`MMR`)\n- human papillomavirus (`HPV`)\n- tetanus, diphtheria and polio (`3IN1`)\n- meningococcal infectious disease (`MENACWY`)\n- respiratory syncytial virus infection (`RSV`)\n \n### Data availability, timing and quality\n\nThis is a real-time service, constrained by the time taken for providers to transfer vaccination events. In most cases, a record will become available within 48 hours of the immunisation event.\n\nThe API search interaction will only return immunisation records based on a traced NHS number. Other interactions require the use of the immunisation ID assigned by the API to interact with individual records for read, update and delete.\n\nThe vaccination events for all disease types are limited to vaccinations administered on behalf of NHS England.\n\nThere is a limited scope of data validation upon receipt of the data. Whilst the data is generally of a good, reliable quality, consumers must be aware that data is shared as received, and users should consider the risk of potential absences or inaccuracies of the data. \n\n \n## Who can use this API \nThis API can only be used where there is a commercial, legal and clinical basis to do so. Make sure you have a valid use case before you go too far with your development.\n\nYou must demonstrate you have a valid use case as part of digital onboarding. \n\nYou must do this before you can go live (see [Onboarding](https://digital.nhs.uk/developer/api-catalogue/immunisation-fhir-api#overview--onboarding) below).\n\n### Who can access immunisation event records\n \nHealth and care organisations in England can access immunisation event records.\n\nLegitimate direct care examples include NHS organisations delivering healthcare, local authorities delivering care, third sector and private sector health and care organisations, and developers delivering systems to health and care organisations.\n\n## API status and roadmap\nThe current roadmap includes enhancements to support all vaccines covered by the national Section 7a immunisation programme.\n \nThis API is in [Beta](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#statuses) for the following vaccine types:\n \n- respiratory syncytial virus (RSV)\n- influenza\n- human papillomavirus (HPV)\n\nThis API is in development for the following vaccine types:\n \n- Measles, Mumps and Rubella (MMR)\n- MenACWY\n- diphtheria, tetanus and polio (3-in-1)\n \nThis API will be configured to support all other section 7a vaccines from early 2026 onwards, adding support for the following vaccine types:\n \n- pneumococcal\n- shingles\n- pertussis\n- coronavirus (COVID-19) vaccinations\n- MMRV\n- diphtheria / tetanus / acellular pertussis / inactivated polio vaccine / haemophilus influenzae type b / hepatitis B (6-in-1)\n- rotavirus\n- meningococcal (MenB)\n- diphtheria / tetanus / acellular pertussis / inactivated polio vaccine (4-in-1)\n- haemophilus influenzae type b / meningococcal group C (Hib/MenC)\n- Bacillus Calmette-Guérin (BCG)\n- hepatitis B\n \nTo suggest new features, comment, or if you have any other queries, [contact us](https://digital.nhs.uk/developer/help-and-support).\n \n## Service level\nThis API will be a platinum service, meaning it is operational and supported 24 x 7 x 365.\n \nFor more details, see [service levels](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#service-levels).\n \n## Technology\n \nThis API is [RESTful](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#basic-rest).\n \nIt conforms to the [FHIR](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) global standard for health care data exchange, specifically to [FHIR R4 (v4.0.1)](https://hl7.org/fhir/r4/), except that it does not support the [capabilities](http://hl7.org/fhir/R4/http.html#capabilities) interaction.\n \nIt includes some country-specific FHIR extensions, which conform to [FHIR UK Core](https://digital.nhs.uk/services/fhir-uk-core), specifically [fhir.r4.ukcore.stu2](https://simplifier.net/packages/fhir.r4.ukcore.stu2).\n \nYou do not need to know much about FHIR to use this API - FHIR APIs are just RESTful APIs that follow specific rules.\nIn particular:\n- resource names are capitalised and singular, and use US spellings, for example `/Immunization` not `/immunisations`\n- array names are singular, for example `entry` not `entries` for address lines\n- data items that are country-specific and thus not included in the FHIR global base resources are usually wrapped in an `extension` object\n \nThere are [libraries and SDKs available](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) to help with FHIR API integration.\n \n## Network access\nThis API is available on the internet and, indirectly, on the [Health and Social Care Network (HSCN)](https://digital.nhs.uk/services/health-and-social-care-network).\n \nFor more details see [Network access for APIs](https://digital.nhs.uk/developer/guides-and-documentation/network-access-for-apis).\n \n## Security and authorisation\n \nThis API currently has a single access mode: [application-restricted access](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation#application-restricted-apis), meaning we authenticate the calling application but not the end user.\n \nTo use this access mode, use the following security pattern:\n- [Application-restricted RESTful API - signed JWT authentication](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication) \n\n## Errors\nWe use standard HTTP status codes to show whether an API request succeeded or not. They are usually in the range:\n\n* 200 to 299 if it succeeded, including code 202 if it was accepted by an API that needs to wait for further action\n* 400 to 499 if it failed because of a client error by your application\n* 500 to 599 if it failed because of an error on our server\n\nErrors specific to each API are shown in the Endpoints section, under Response. See our [reference guide](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#http-status-codes) for more on errors.\n\n## Open source\n\nYou might find the following [open source](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#open-source) resources useful:\n\n| Resource | Description | Links |\n|---------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------|\n| Immunisation FHIR API | Source code for the API proxy, sandbox and specification. | [GitHub repo](https://github.dev/NHSDigital/immunisation-fhir-api/) |\n| FHIR libraries and SDKs | Various open source libraries for integrating with FHIR APIs. | [FHIR libraries and SDKs](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) |\n| nhs-number | Python package containing utilities for NHS numbers including validity checks, normalisation and generation. | [GitHub repo](https://github.com/uk-fci/nhs-number) \\| [Python Package index](https://pypi.org/project/nhs-number/) \\| [Docs](https://nhs-number.uk-fci.tech/) |\n\n## Environments and Testing\n| Environment | Base URL |\n| ----------------- | --------------------------------------------------------------------- |\n| Sandbox | `https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n| Integration | `https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` | \n| Production | `https://api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n\n### Sandbox testing\nOur [sandbox environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#sandbox-testing):\n* is for early developer testing\n* only covers a limited set of scenarios\n* is stateless, so does not actually persist any updates\n* is open access, so does not allow you to test authorisation\n\nFor details of sandbox test scenarios, or to try out the sandbox using our 'Try this API' feature, see the documentation for each endpoint.\n\n### Integration testing\nOur [integration test environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing):\n* is for formal integration testing\n* is stateful, so persists updates\n* includes authorisation, with options for application-restricted access \n\nFor read-only testing, we will provide an Immunisation records test pack soon.\n\nTo test creating, updating and deleting patient vaccination events, you must set up your own test data.\n\nFor more details see [integration testing with our RESTful APIs](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing-with-our-restful-apis).\n\n## Onboarding\n \nYou need to get your software approved by us before it can go live with this API.\nWe call this onboarding.\nThe onboarding process can sometimes be quite long, so it's worth planning well ahead.\n\nThis API is currently in private Beta, but we expect to open it to new consumers soon. As part of this process, you need to demonstrate that you can manage risks and that your software conforms technically with the requirements for this API. Information on this page might impact the design of your software.\n\nTo understand how our online digital onboarding process works, see [digital onboarding](https://digital.nhs.uk/developer/guides-and-documentation/digital-onboarding#using-the-digital-onboarding-portal).\n\n## Related APIs\n\nThe following APIs are related to this API:\n\n### Immunisation History - FHIR API\nUse the [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) if you want to access vaccination records that are not yet available on this API.\n\n## Contact us\nFor help and support connecting to our APIs and to join our developer community, see [Help and support building healthcare software](https://digital.nhs.uk/developer/help-and-support). \n" - }, - "servers": [ - { - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", - "description": "Sandbox Server" - }, - { - "url": "https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", - "description": "Integration Server" - } - ], - "paths": { - "/Immunization": { - "post": { - "summary": "Record a vaccination given to a patient", - "operationId": "createImmunization", - "description": "## Overview\nUse this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the disease types enabled in this interaction and represented by the correct SNOMED concept(s) for that disease type. A [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) is provided for supported disease types. \nYou must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n | Record a vaccination event | Valid request as per schema | HTTP Status 201 with immunisation id in response header (location) |\n| Bad Request (missing/invalid required element in request body) | Didn't pass `resourceType` in request body | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - } - ], - "requestBody": { - "content": { - "application/fhir+json": { - "schema": { - "description": "A FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrence[X]", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "meta": { - "type": "object", - "description": "Metadata about the resource.", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - } - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." - } - } - } - }, - "name": { - "type": "array", - "description": "Patient name as registered, or as recorded by the user where the patient record cannot be traced. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "name", - "gender", - "birthDate", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - }, - "text": { - "description": "Plain text representation of the concept.", - "type": "string" - } - } - } - } - } - }, - "identifier": { - "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "A URI for the system that has allocated the vaccination identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" - }, - "value": { - "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrence[X]": { - "type": "object", - "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", - "properties": { - "occurrenceDateTime": { - "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \n Only positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - } - }, - "required": [ - "occurrenceDateTime" - ] - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - } - }, - "manufacturer": { - "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", - "type": "object", - "properties": { - "display": { - "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - } - }, - "route": { - "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", - "type": "object", - "properties": { - "value": { - "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", - "type": "number", - "example": 1 - }, - "unit": { - "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", - "type": "object", - "properties": { - "type": { - "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", - "type": "string", - "example": "B0C4P" - } - } - }, - "reference": { - "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", - "type": "string", - "example": "#Pract1" - } - } - } - } - } - }, - "reasonCode": { - "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", - "type": "array", - "items": { - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. See the provided [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - } - } - } - } - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available.. Maximum value is 9.", - "type": "integer", - "maximum": 9, - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "a7437179-e86e-4855-b68e-24b5jhg3g" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07T13:28:17.271+00:00", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "responses": { - "201": { - "description": "Create Immunization operation successful", - "headers": { - "Location": { - "$ref": "#/components/headers/Location" - }, - "CorrelationID": { - "$ref": "#/components/headers/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/headers/RequestID" - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms-create" - } - } - }, - "get": { - "summary": "Search for a patient's immunisation records", - "operationId": "searchImmunization", - "description": "## Overview\nUse this interaction to search for a patient's vaccination records using their NHS number and DiseaseType. You can request the patient's vaccination history for one or more specified 'disease types'. You may limit the vaccination records by specifying date criteria, for example if you only need to know about vaccinations administered in the last 12 months. \n Location related data items are included. Patient location sensitivity indicators (such as flags for sensitive patient records) should be used to apply data filtering as appropriate. The response will not include contained resources for patient or practitioner within each immunization resource it returns. A single, separate patient resource will be included in the bundle and referenced by each immunization. \nVaccination events submitted without an NHS Number will not be available for retrieval via this interaction. Also, where a patient has a change of NHS Number some or all records may be unavailable via this interaction for a short period of time while records are updated. \nYou must be authorised for the search interaction and the disease type(s) specified in your search in order to access the records. \n \n### Search using POST\n \nA POST search interaction is supported in accordance with [FHIR guidance](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) as an alternative to a search with the GET verb. A POST search allows you to supply some or all parameters in the body of the request should you need to do so. It offers the same search functionality as the GET search interaction. \n\nNote that the API call for the POST search is different: \n\n`POST /Immunization/_search`\n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=https://fhir.nhs.uk/Id/nhs-number|9000000009 | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass required fields `patient.identifier` or `-immunization.target` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/PatientIdentifier" - }, - { - "$ref": "#/components/parameters/ImmunizationTarget" - }, - { - "$ref": "#/components/parameters/DateFrom" - }, - { - "$ref": "#/components/parameters/DateTo" - }, - { - "$ref": "#/components/parameters/Include" - } - ], - "responses": { - "200": { - "description": "Search immunisation operation successful", - "content": { - "application/fhir+json": { - "schema": { - "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", - "type": "object", - "required": [ - "resourceType", - "type", - "total", - "entry" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Bundle`.", - "type": "string", - "example": "Bundle" - }, - "type": { - "description": "Indicates how the bundle is intended to be used. Always `searchset`.", - "type": "string", - "example": "searchset" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "relation": { - "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1). Always `Self`.", - "type": "string" - }, - "url": { - "description": "A url representing the search applied by the API to generate the result which may differ from the request if unrecognised or unsupported parameters have been ignored.", - "type": "string" - } - }, - "required": [ - "relation", - "url" - ] - } - }, - "entry": { - "description": "List of matching immunisations and associated patient. If there were no matching immunisations, this is an empty list.", - "type": "array", - "items": { - "type": "object", - "required": [ - "fullUrl", - "resource", - "search" - ], - "properties": { - "fullUrl": { - "description": "URI for the Immunization or Patient resource.", - "type": "string", - "example": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "resource": { - "description": "The Immunization or Patient resource.", - "oneOf": [ - { - "description": "A matching immunisation, formatted as a FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrence[X]", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "191f288a-17f3-4cd5-a33c-a52aade6473c" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "Identifies where the resource comes from." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - }, - "required": [ - "versionId" - ] - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - } - } - } - } - } - }, - "identifier": { - "description": "Unique identifier for this immunisation record, as generated by the source system.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "URI of the namespace of this identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc" - }, - "value": { - "description": "Identifier value within `system`.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who was immunised.", - "type": "object", - "required": [ - "reference", - "type", - "identifier" - ], - "properties": { - "reference": { - "description": "URI for the associated Patient resource in the bundle.", - "type": "string", - "example": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" - }, - "type": { - "description": "Type of resource this reference refers to. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "identifier": { - "description": "Business identifier for linked Patient. Always an NHS number.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "URI of coding system used to identify linked patient. Always https://fhir.nhs.uk/Id/nhs-number", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Value in coding system representing linked patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - }, - "occurrence[X]": { - "type": "object", - "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", - "properties": { - "occurrenceDateTime": { - "description": "Date and time of immunisation.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - } - }, - "required": [ - "occurrenceDateTime" - ] - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "required": [ - "identifier" - ] - }, - "manufacturer": { - "description": "Vaccine manufacturer details.", - "type": "object", - "properties": { - "display": { - "description": "Decsription of the vaccine manufacturer.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Lot number of the vaccine product.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Date vaccine batch expires.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - }, - "required": [ - "coding" - ] - }, - "route": { - "description": "The path by which the vaccine product is taken into the body.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered.", - "type": "object", - "properties": { - "value": { - "description": "Number of units administered.", - "type": "number", - "example": 1 - }, - "unit": { - "description": "Description of unit.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "System that defines coded unit form.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "Code describing the unit.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "Organisation that performed the immunisation.", - "type": "object", - "required": [ - "type", - "identifier" - ], - "properties": { - "type": { - "description": "Type of actor. Always `Organisation`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "Organisation identifier.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "Organisation's ODS code.", - "type": "string", - "example": "B0C4P" - } - } - }, - "display": { - "description": "Organisation that performed the immunisation.", - "type": "string", - "example": "UNIVERSITY HOSPITAL OF WALES" - } - } - } - } - } - }, - "reasonCode": { - "description": "Reasons why the vaccine was administered.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against.", - "items": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - }, - "required": [ - "system", - "code", - "display" - ] - } - } - }, - "required": [ - "coding" - ] - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", - "type": "integer", - "maximum": 9, - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - { - "description": "Demographic information about the patient receiving an immunisation.", - "type": "object", - "required": [ - "resourceType", - "id" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Patient`.", - "type": "string", - "example": "Patient" - }, - "id": { - "description": "Patient ID (NHS Number)", - "type": "string", - "example": "9000000009" - }, - "identifier": { - "description": "Unique identifier for this patient. Always an NHS number.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used to identify patients.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number" - }, - "value": { - "description": "Code identifying the patient.", - "type": "string", - "example": "9000000009" - } - } - } - } - } - } - ] - }, - "search": { - "description": "Search-related information for the Immunization.", - "type": "object", - "required": [ - "mode" - ], - "properties": { - "mode": { - "description": "Indicates why this resource is in the result set. For Immunization resources this is always `match`.", - "enum": [ - "match", - "include" - ] - } - } - } - } - } - }, - "total": { - "description": "Number of matching immunisations found.", - "type": "integer", - "example": 2 - } - } - }, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "link": [ - { - "relation": "self", - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=RSV&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9000000009" - } - ], - "entry": [ - { - "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", - "resource": { - "resourceType": "Immunization", - "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", - "meta": { - "versionId": "1" - }, - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - }, - "display": "UNIVERSITY HOSPITAL OF WALES" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005", - "display": "Disease outbreak (event)" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "resource": { - "resourceType": "Patient", - "id": "9000000009", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009" - } - ] - }, - "search": { - "mode": "include" - } - } - ], - "total": 1 - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms-search" - } - } - } - }, - "/Immunization/{id}": { - "get": { - "summary": "Retrieve a record of an immunisation by its unique identifier", - "operationId": "readImmunization", - "description": "## Overview\nThis interaction allows you to retrieve the record of a single vaccination by our assigned id. We will return the full immunization resource as submitted. \nThe response will include an eTag for the version of the record which has been returned. If you intend to update a record, it is recommended that you use this interaction to obtain the latest version (and eTag for the version). \nTo retrieve a full vaccination history for a patient, see the search interaction. \nYou must be authorised for the read interaction and the disease type associated with the vaccination event in order to access the record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation record found | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass required fields `id` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "description": "Read Immunization operation successful", - "headers": { - "CorrelationID": { - "$ref": "#/components/headers/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/headers/RequestID" - }, - "E-Tag": { - "$ref": "#/components/headers/E-Tag" - } - }, - "content": { - "application/fhir+json": { - "schema": { - "description": "A matching immunisation, formatted as a FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrence[X]", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "The schema for Practitioner & Patient are different. In response both Practitioner & Patient objects will be returned.", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." - } - } - } - }, - "name": { - "type": "array", - "description": "A name associated with the patient", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "An address for the individual", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Postal code for area" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "name", - "gender", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - } - } - } - } - } - }, - "identifier": { - "description": "Unique identifier for this immunisation record, as generated by the source system.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "URI of the namespace of this identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc" - }, - "value": { - "description": "Identifier value within `system`.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who was immunised.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrence[X]": { - "type": "object", - "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", - "properties": { - "occurrenceDateTime": { - "description": "Date and time of immunisation.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - } - }, - "required": [ - "occurrenceDateTime" - ] - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", - "type": "string", - "example": "X99999" - } - }, - "required": [ - "system", - "value" - ] - } - }, - "required": [ - "identifier" - ] - }, - "manufacturer": { - "description": "Vaccine manufacturer details.", - "type": "object", - "properties": { - "display": { - "description": "Decsription of the vaccine manufacturer.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Lot number of the vaccine product.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Date vaccine batch expires.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - }, - "required": [ - "coding" - ] - }, - "route": { - "description": "The path by which the vaccine product is taken into the body.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered.", - "type": "object", - "properties": { - "value": { - "description": "Number of units administered.", - "type": "number", - "example": 1 - }, - "unit": { - "description": "Description of unit.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "System that defines coded unit form.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "Code describing the unit.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "Organisation that performed the immunisation.", - "type": "object", - "required": [ - "type", - "identifier" - ], - "properties": { - "type": { - "description": "Type of actor. Always `Organisation`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "Organisation identifier.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "Organisation's ODS code.", - "type": "string", - "example": "B0C4P" - } - } - }, - "display": { - "description": "Organisation that performed the immunisation.", - "type": "string", - "example": "UNIVERSITY HOSPITAL OF WALES" - } - } - } - } - } - }, - "reasonCode": { - "description": "Reasons why the vaccine was administered.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "code", - "display" - ], - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against.", - "items": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - }, - "required": [ - "system", - "code", - "display" - ] - } - } - }, - "required": [ - "coding" - ] - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", - "type": "integer", - "maximum": 9, - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Owl", - "given": [ - "Barney" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Owler", - "given": [ - "Ozzie" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "X99999" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms-read" - } - } - }, - "put": { - "summary": "Update a record of vaccination", - "operationId": "updateImmunization", - "description": "## Overview\nThis interaction allows you to add a new updated record of a vaccination event. Update replaces the full immunization resource, so you must provide all data fields, not just the change (Patch is not currently supported). You may obtain all the current data for the vaccination event using the read interaction, which will also return an eTag for the version. \nYou may use update to re-instate a deleted record, but our update interaction does not support creating a new vaccination event where one does not currently exist. \nYou must not change the identifier when updating a vaccination event. The identifier is used as a primary identifier by downstream systems. \nYou must be authorised for update interaction and the disease type associated with the vaccination event in order to update the record. \n\n## Sandbox testing \n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Update a vaccination event | Valid request as per schema | HTTP Status 200 |\n| Bad Request (missing/invalid required element in request body) | Didn't pass `E-Tag` in request header | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - }, - { - "$ref": "#/components/parameters/E-Tag" - } - ], - "requestBody": { - "content": { - "application/fhir+json": { - "schema": { - "description": "A FHIR Immunization resource.", - "type": "object", - "required": [ - "resourceType", - "id", - "contained", - "extension", - "identifier", - "status", - "vaccineCode", - "patient", - "occurrence[X]", - "recorded", - "primarySource", - "location", - "performer", - "protocolApplied" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Immunization`.", - "type": "string", - "example": "Immunization" - }, - "id": { - "description": "Immunization record Id.", - "type": "string", - "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" - }, - "meta": { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed.", - "example": "2017-01-01T00:00:00Z" - }, - "source": { - "type": "string", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - } - }, - "contained": { - "type": "array", - "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Practitioner`.", - "example": "Practitioner" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact" - }, - "name": { - "type": "array", - "description": "The name(s) associated with the practitioner", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Given names (not always 'first').", - "items": { - "type": "string" - } - } - } - } - } - }, - "required": [ - "resourceType", - "id" - ] - }, - { - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "description": "FHIR resource type. Always `Patient`.", - "example": "Patient" - }, - "id": { - "type": "string", - "description": "Logical id of this artifact", - "example": "#Pat1" - }, - "identifier": { - "type": "array", - "description": "An identifier for the patient", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string", - "description": "The namespace for the identifier value" - }, - "value": { - "type": "string", - "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." - } - } - } - }, - "name": { - "type": "array", - "description": "Patient name as registered, or as recorded by the user where the patient record cannot be traced. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", - "items": { - "type": "object", - "properties": { - "family": { - "type": "string", - "description": "Family name (often called 'Surname')" - }, - "given": { - "type": "array", - "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", - "items": { - "type": "string" - } - } - }, - "required": [ - "family", - "given" - ] - } - }, - "gender": { - "type": "string", - "description": "male | female | other | unknown" - }, - "birthDate": { - "type": "string", - "description": "The date of birth for the individual" - }, - "address": { - "type": "array", - "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", - "items": { - "type": "object", - "properties": { - "postalCode": { - "type": "string", - "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" - } - }, - "required": [ - "postalCode" - ] - } - } - }, - "required": [ - "resourceType", - "id", - "name", - "gender", - "birthDate", - "address" - ] - } - ] - } - }, - "extension": { - "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "url", - "valueCodeableConcept" - ], - "properties": { - "url": { - "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "type": "string", - "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" - }, - "valueCodeableConcept": { - "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "description": "Wrapper for the vaccination procedure coding.", - "type": "array", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "A particular code in the system.", - "type": "string", - "example": "1303503001" - }, - "display": { - "description": "Representation defined by the system.", - "type": "string", - "example": "Administration of RSV (respiratory syncytial virus) vaccine" - } - } - } - }, - "text": { - "description": "Plain text representation of the concept.", - "type": "string" - } - } - } - } - } - }, - "identifier": { - "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "use": { - "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", - "type": "string", - "enum": [ - "usual", - "official", - "temp", - "secondary", - "old" - ], - "example": "official" - }, - "system": { - "description": "A URI for the system that has allocated the vaccination identifier.", - "type": "string", - "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" - }, - "value": { - "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", - "type": "string", - "example": "e2154d29-1ead-4830-a513-0d59705078fa" - } - } - } - }, - "status": { - "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", - "type": "string", - "enum": [ - "completed" - ], - "example": "completed" - }, - "vaccineCode": { - "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccine product details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccine product.", - "type": "string", - "example": "42605811000001109" - }, - "display": { - "description": "Description of the vaccine product.", - "type": "string", - "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - } - } - } - } - }, - "patient": { - "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "description": "Reference of patient from contained section", - "type": "string", - "example": "#Pat1" - } - } - }, - "occurrence[X]": { - "type": "object", - "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", - "properties": { - "occurrenceDateTime": { - "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \n Only positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - } - }, - "required": [ - "occurrenceDateTime" - ] - }, - "recorded": { - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", - "type": "string", - "example": "2021-02-07T13:28:17.271000+00:00" - }, - "primarySource": { - "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", - "type": "boolean", - "example": true - }, - "location": { - "type": "object", - "description": "The service delivery location where the vaccine administration occurred.", - "properties": { - "identifier": { - "type": "object", - "description": "An identifier for the service delivery location.", - "properties": { - "system": { - "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", - "type": "string", - "example": "urn:iso:std:iso:3166" - }, - "value": { - "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", - "type": "string", - "example": "GB" - } - }, - "required": [ - "system", - "value" - ] - } - } - }, - "manufacturer": { - "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", - "type": "object", - "properties": { - "display": { - "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "AstraZeneca Ltd" - } - } - }, - "lotNumber": { - "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", - "type": "string", - "example": "4120Z001" - }, - "expirationDate": { - "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "2021-04-29" - }, - "site": { - "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination body site details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination body site.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination body site.", - "type": "string", - "example": "368208006" - }, - "display": { - "description": "Description of the vaccination body site.", - "type": "string", - "example": "Left upper arm structure (body structure)" - } - } - } - } - } - }, - "route": { - "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the vaccination route details.", - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe vaccination route.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "Code for the vaccination route.", - "type": "string", - "example": "78421000" - }, - "display": { - "description": "Description of the vaccination route.", - "type": "string", - "example": "Intramuscular route (qualifier value)" - } - } - } - } - } - }, - "doseQuantity": { - "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", - "type": "object", - "properties": { - "value": { - "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", - "type": "number", - "example": 1 - }, - "unit": { - "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "milliliter" - }, - "system": { - "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "http://unitsofmeasure.org" - }, - "code": { - "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", - "type": "string", - "example": "ml" - } - } - }, - "performer": { - "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "actor" - ], - "properties": { - "actor": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", - "type": "object", - "properties": { - "type": { - "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", - "type": "string", - "example": "Organisation" - }, - "identifier": { - "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", - "type": "object", - "required": [ - "system", - "value" - ], - "properties": { - "system": { - "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", - "type": "string", - "example": "https://fhir.nhs.uk/Id/ods-organization-code" - }, - "value": { - "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", - "type": "string", - "example": "B0C4P" - } - } - }, - "reference": { - "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", - "type": "string", - "example": "#Pract1" - } - } - } - } - } - }, - "reasonCode": { - "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", - "type": "array", - "items": { - "type": "object", - "properties": { - "coding": { - "description": "Wrapper for the reason code details.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "system": { - "description": "Coding system used to describe the reason for administration of vaccine.", - "type": "string", - "example": "http://snomed.info/sct" - }, - "code": { - "description": "SNOMED code for the vaccination reason.", - "type": "string", - "example": "443684005" - }, - "display": { - "description": "Description of the vaccination reason.", - "type": "string", - "example": "Disease outbreak (event)" - } - } - } - } - } - } - }, - "protocolApplied": { - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "targetDisease", - "doseNumber[X]" - ], - "properties": { - "targetDisease": { - "type": "array", - "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. See the provided [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", - "items": { - "type": "object", - "required": [ - "coding" - ], - "properties": { - "coding": { - "type": "array", - "description": "A reference to a code defined by a terminology system.", - "items": { - "type": "object", - "required": [ - "system", - "code" - ], - "properties": { - "system": { - "description": "The identification of the code system that defines the meaning of the symbol in the code.", - "type": "string" - }, - "code": { - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", - "type": "string" - }, - "display": { - "description": "A representation of the meaning of the code in the system, following the rules of the system.", - "type": "string" - } - } - } - } - } - } - }, - "doseNumber[X]": { - "type": "object", - "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", - "properties": { - "doseNumberPositiveInt": { - "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", - "type": "integer", - "maximum": 9, - "example": 1 - }, - "doseNumberString": { - "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", - "type": "string" - } - } - } - } - } - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "4ff607e0-c6e9-4fe0-a2b6-3bcd7fdc44c9", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Nightingale", - "given": [ - "Florence" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Taylor", - "given": [ - "Sarah" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "EC1A 1BB" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1324681000000101", - "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" - } - ] - } - } - ], - "identifier": [ - { - "system": "https://supplierABC/identifiers/vacc", - "value": "a7437179-e86e-4855-b68e-24b5jhg3g" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39114911000001105", - "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", - "recorded": "2021-02-07T13:28:17.271+00:00", - "primarySource": true, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "location": { - "identifier": { - "value": "X99999", - "system": "https://fhir.nhs.uk/Id/ods-organization-code" - } - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "code": "443684005", - "system": "http://snomed.info/sct" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Update Immunization operation successful", - "headers": { - "CorrelationID": { - "$ref": "#/components/headers/CorrelationID" - }, - "RequestID": { - "$ref": "#/components/headers/RequestID" - } - } - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms-update" - } - } - }, - "delete": { - "summary": "Mark a record of vaccination as being entered in error", - "operationId": "deleteImmunization", - "description": "## Overview\nThis interaction allows you to mark a record that has been entered in error.\nDeleted records will continue to be stored for a period of time but are not returned in response to read or search requests.\nA deleted record can be re-instated using the update interaction if it was incorrectly deleted. \n\n## Sandbox testing\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Delete a vaccination event | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 204 No Content |\n| Bad Request | Didn't pass required fields `id` | HTTP Status 400 Bad Request |\n", - "parameters": [ - { - "$ref": "#/components/parameters/CorrelationID" - }, - { - "$ref": "#/components/parameters/RequestID" - }, - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "204": { - "description": "Delete Immunization operation successful" - }, - "4XX": { - "$ref": "#/components/responses/4XX-imms-delete" - } - } - } - } - }, - "components": { - "responses": { - "4XX-imms-create": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Invalid resourceType in body | {\"resourceType\": \"OperationOutcome\", \"id\": \"a7dc58e7-4033-43e6-b34e-cf7ee7bbc567\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: This service only accepts FHIR Immunization Resources (i.e. resourceType must equal 'Immunization')\"}]} |\n| 400 | Bad Request | Invalid resourceType within contained | {\"resourceType\": \"OperationOutcome\", \"id\": \"ffda54b5-21a7-4e4c-9ca5-0a4e510d7467\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: contained must contain only Patient and Practitioner resources\"}]} |\n| 400 | Bad Request | Invalid status value | {\"resourceType\": \"OperationOutcome\", \"id\": \"12a9f94c-df00-4e87-aefa-2c76f9065367\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: status must be one of the following: completed\"}]} |\n| 400 | Bad Request | Invalid value for a datetime (string) field e.g. occurrenceDateTime.
Note : The error format will remain same for any datetime (string) field; only the field name will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"e02e3254-c1b3-4afd-a8ca-84091d7d272c\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: occurrenceDateTime must be a valid datetime in the format 'YYYY-MM-DDThh:mm:ss+zz:zz' (where time element is optional, timezone must be given if and only if time is given, and milliseconds can be optionally included after the seconds). Note that partial dates are not allowed for occurrenceDateTime for this service.\"}]} |\n| 400 | Bad Request | Invalid value for a string field e.g. postalCode
Note : The error format will remain same for any string field; only the field location will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"0df23485-4690-41ab-8b24-8b28584ec2eb\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: contained[?(@.resourceType=='Patient')].address[0].postalCode must be a non-empty string\"}]} |\n| 400 | Bad Request | Invalid value for an integer field e.g. doseNumberPositiveInt
Note : The error format will remain same for any integer field; only the field location will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"dda6b6cd-af06-47ea-a4d4-f0f8cdf83085\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: protocolApplied[0].doseNumberPositiveInt must be a positive integer\"}]} |\n| 400 | Bad Request | Invalid top level element e.g. test | {\"resourceType\": \"OperationOutcome\", \"id\": \"fd60f75c-d2cb-4c77-b3e9-ba8c3e0379e6\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: \"test\" is not an allowed element of the Immunization resource for this service\"}]} |\n| 400 | Bad Request | Missing mandatory field e.g. contained
Note : The error format will remain same for any mandatory field; only the field name will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"72eee803-c8bf-4728-a15b-5d9a10bb645c\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"contained is a mandatory field\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 422 | Unprocessable Entity | Duplicate Identifier value | {\"resourceType\": \"OperationOutcome\", \"id\": \"b82adf54-1844-4354-a5dd-09bfc34dd569\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"duplicate\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"DUPLICATE\"}]}, \"diagnostics\": \"The provided identifier: https://supplierABC/identifiers/vacc#a7437179-e86e-4855-b68e-xxxxx is duplicated\"}]} |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType":"OperationOutcome", - "id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta":{ - "profile":[ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue":[ - { - "severity":"error", - "code":"expired", - "details":{ - "coding":[ - { - "system":"https://fhir.nhs.uk/Codesystem/http-error-codes", - "code":"SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } - } - } - }, - "4XX-imms-search": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Search parameter immunization.target is either missing or not in the expected format. | {\"resourceType\":\"OperationOutcome\",\"id\":\"0f985bbd-a25e-4644-bcab-e11ac9f1cf3a\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"Search parameter -immunization.target must have one or more values.\"}]} |\n| 400 | Bad Request | Search parameter patient.identifier is either missing or not in the expected format. | {\"resourceType\":\"OperationOutcome\",\"id\":\"ee3ce747-95dc-40e9-be2e-404edf203444\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"Search parameter patient.identifier must have one value.\"}]} |\n| 400 | Bad Request | Invalid value for patient.identifier | {\"resourceType\":\"OperationOutcome\",\"id\":\"ec7f1d44-6658-42cc-bffb-af790beba382\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"patient.identifier must be in the format of \\\"https://fhir.nhs.uk/Id/nhs-number|{NHS number}\\\" e.g. \\\"https://fhir.nhs.uk/Id/nhs-number|9000000009\\\"\"}]} |\n| 400 | Bad Request | Invalid date.to/date.from format | {\"resourceType\": \"OperationOutcome\", \"id\": \"ad3d7881-6f2e-418f-ae13-75a07d7269d7\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"Search parameter -date.to must be in format: YYYY-MM-DD\"}]} |\n| 400 | Bad Request | Invalid value for \"-immunization.target\" | {\"resourceType\": \"OperationOutcome\", \"id\": \"f1753822-5667-4562-a288-1544a0b66d00\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"immunization-target must be one or more of the following: COVID19,FLU,HPV,MMR,3IN1,MENACWY,RSV\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType":"OperationOutcome", - "id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta":{ - "profile":[ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue":[ - { - "severity":"error", - "code":"expired", - "details":{ - "coding":[ - { - "system":"https://fhir.nhs.uk/Codesystem/http-error-codes", - "code":"SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } - } - } - }, - "4XX-imms-read": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Missing immunization event identifier (id) | {\"resourceType\": \"OperationOutcome\", \"id\": \"438f5c0d-f89d-46ec-b4ba-c08dc5a44ff3\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Unrecognized immunization event identifier (id) | {\"resourceType\": \"OperationOutcome\", \"id\": \"5b51e331-5f9c-442f-a4c3-b5f52a1ba83e\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"The requested resource was not found.\"}]} |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType":"OperationOutcome", - "id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta":{ - "profile":[ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue":[ - { - "severity":"error", - "code":"expired", - "details":{ - "coding":[ - { - "system":"https://fhir.nhs.uk/Codesystem/http-error-codes", - "code":"SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } - } - } - }, - "4XX-imms-update": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | All validation errors & mandatory field errors from the Record scenario | All validation errors & mandatory field errors from POST /Immunization |\n| 400 | Bad Request | Missing id in request parameter | {\"resourceType\": \"OperationOutcome\", \"id\": \"05ef00c0-18ae-4a88-93ca-9dc1b0532ea1\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 400 | Bad Request | Missing id parameter in request body
or
mismatch between id provided within request body and request parameter | {\"resourceType\": \"OperationOutcome\", \"id\": \"ddf659ac-1a34-4ca2-b11a-77b9a8febeec\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The provided immunization id:16ec10f2-afef-4c0b-9467-xxxx doesn't match with the content of the request body\"}]} |\n| 400 | Bad Request | Mismatch between identifier value and stored event | {\"resourceType\": \"OperationOutcome\", \"id\": \"ac43b4a6-7ceb-4ff8-a246-d7ceacdab058\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: identifier[0].value doesn't match with the stored content\"}]} |\n| 400 | Bad Request | Missing E-Tag (version) header | {\"resourceType\": \"OperationOutcome\", \"id\": \"715d5e23-4621-4719-8902-e80faf6539fd\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: Immunization resource version not specified in the request headers\"}]} |\n| 400 | Bad Request | Wrong version number passed in E-Tag header
e.g. passing version no. > 1 in case of first-time update | {\"resourceType\": \"OperationOutcome\", \"id\": \"7cf8b2e1-f069-4cce-9b48-5201f64a50a9\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource 5a64fa79-9114-49bb-97c9-d455b0276475 version is inconsistent with the existing version.\"}]} |\n| 400 | Bad Request | Empty value for E-Tag header | {\"resourceType\": \"OperationOutcome\", \"id\": \"6eda36fc-777a-472a-b3a3-3e1627eca980\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: Immunization resource version: in the request headers is invalid.\"}]} |\n| 400 | Bad Request | E-Tag value <= current stored value | {\"resourceType\": \"OperationOutcome\", \"id\": \"06e3086a-61b5-4dfc-b0ff-10e2658268f5\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource 5a64fa79-9114-49bb-97c9-d455b0276475 has changed since the last retrieve.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Provided id not available | {\"resourceType\": \"OperationOutcome\", \"id\": \"c86be3de-bcbb-4ba5-902e-9acf55187dc5\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource with id:16ec10f2-afef-4c0b-9467-80434a7a0e26 was not found.\"}]} |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType":"OperationOutcome", - "id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta":{ - "profile":[ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue":[ - { - "severity":"error", - "code":"expired", - "details":{ - "coding":[ - { - "system":"https://fhir.nhs.uk/Codesystem/http-error-codes", - "code":"SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } - } - } - }, - "4XX-imms-delete": { - "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Missing or invalid id | {\"resourceType\": \"OperationOutcome\", \"id\": \"f400ad2c-62d9-4f98-bc35-a146caf14dee\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Non existing id in query parameter
or
Trying to deleted an already deleted record | {\"resourceType\": \"OperationOutcome\", \"id\": \"f772ee3e-3baf-4934-a07a-a13f70b1a50e\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"Immunization resource does not exist. ID: 5a64fa79-9114-49bb-97c9-xxxxxxxx\"}]} |\n", - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/OperationOutcome" - }, - "example": { - "resourceType":"OperationOutcome", - "id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d", - "meta":{ - "profile":[ - "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" - ] - }, - "issue":[ - { - "severity":"error", - "code":"expired", - "details":{ - "coding":[ - { - "system":"https://fhir.nhs.uk/Codesystem/http-error-codes", - "code":"SEND_UNAUTHORIZED" - } - ] - }, - "diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid." - } - ] - } - } - } - } - }, - "requestBodies": { - "Immunization": { - "content": { - "application/fhir+json": { - "schema": { - "$ref": "#/components/schemas/Immunization" - } - } - }, - "required": true - }, - "SearchImmunization": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "type": "object", - "properties": { - "patient.identifier": { - "type": "string", - "description": "The patient's NHS number.\nExpressed as `` where`` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", - "example": "9000000009" - }, - "-immunization.target": { - "type": "string", - "description": "Specific procedures, disorders, diseases, infections or organisms.\n", - "enum": [ - "COVID19", - "FLU", - "MMR", - "HPV", - "3IN1", - "MENACWY", - "RSV" - ] - }, - "-date.from": { - "type": "string", - "format": "date", - "description": "The earliest date to be included (e.g. 2020-01-01)", - "default": "1900-01-01" - }, - "-date.to": { - "type": "string", - "format": "date", - "description": "The latest date to be included (e.g. 2020-12-31)", - "default": "9999-12-31" - }, - "_include": { - "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", - "type": "string", - "default": "Immunization:patient" - } - } - } - } - } - } - }, - "headers": { - "CorrelationID": { - "required": false, - "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "RequestID": { - "required": false, - "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "Location": { - "required": true, - "description": "The location of the newly created Immunization record. It contains the resource ID at the end.", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "E-Tag": { - "required": true, - "description": "Indicates the current version of an Immunization resource", - "schema": { - "type": "integer", - "example": 1 - } - } - }, - "parameters": { - "Id": { - "in": "path", - "name": "id", - "required": true, - "description": "A required ID which you can use to identify an Immunization event object.\n\nMirrored back in a response header.\n", - "schema": { - "type": "string", - "example": "29dc4e84-7e72-11ee-b962-0242ac120002" - } - }, - "CorrelationID": { - "in": "header", - "name": "X-Correlation-ID", - "required": false, - "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "RequestID": { - "in": "header", - "name": "X-Request-ID", - "required": false, - "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", - "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" - } - }, - "PatientIdentifier": { - "in": "query", - "name": "patient.identifier", - "description": "The patient's NHS number.\nExpressed as `|` where `` must be `https://fhir.nhs.uk/Id/nhs-number` and `` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", - "required": true, - "schema": { - "type": "string", - "example": "https://fhir.nhs.uk/Id/nhs-number|9000000009" - } - }, - "ImmunizationTarget": { - "in": "query", - "name": "-immunization.target", - "description": "Specific procedures, disorders, diseases, infections or organisms.\n", - "required": true, - "schema": { - "type": "string", - "enum": [ - "COVID19", - "FLU", - "MMR", - "HPV", - "3IN1", - "MENACWY", - "RSV" - ] - } - }, - "DateFrom": { - "in": "query", - "name": "-date.from", - "description": "The earliest date to be included (e.g. 2020-01-01)", - "schema": { - "type": "string", - "format": "date", - "default": "1900-01-01" - } - }, - "DateTo": { - "in": "query", - "name": "-date.to", - "description": "The latest date to be included (e.g. 2020-12-31)", - "schema": { - "type": "string", - "format": "date", - "default": "9999-12-31" - } - }, - "Include": { - "in": "query", - "name": "_include", - "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", - "required": false, - "schema": { - "type": "string", - "default": "Immunization:patient" - } - }, - "E-Tag": { - "in": "header", - "name": "E-Tag", - "required": true, - "description": "Indicates the current version of an Immunization resource", - "schema": { - "type": "integer", - "example": 1 - } - } - }, - "schemas": { - "Resource": { - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "enum": [ - "Resource", - "DomainResource", - "Account", - "ActivityDefinition", - "AdverseEvent", - "AllergyIntolerance", - "Appointment", - "AppointmentResponse", - "AuditEvent", - "Basic", - "Binary", - "BiologicallyDerivedProduct", - "BodyStructure", - "Bundle", - "CapabilityStatement", - "CarePlan", - "CareTeam", - "CatalogEntry", - "ChargeItem", - "ChargeItemDefinition", - "Claim", - "ClaimResponse", - "ClinicalImpression", - "CodeSystem", - "Communication", - "CommunicationRequest", - "CompartmentDefinition", - "Composition", - "ConceptMap", - "Condition", - "Consent", - "Contract", - "Coverage", - "CoverageEligibilityRequest", - "CoverageEligibilityResponse", - "DetectedIssue", - "Device", - "DeviceDefinition", - "DeviceMetric", - "DeviceRequest", - "DeviceUseStatement", - "DiagnosticReport", - "DocumentManifest", - "DocumentReference", - "EffectEvidenceSynthesis", - "Encounter", - "Endpoint", - "EnrollmentRequest", - "EnrollmentResponse", - "EpisodeOfCare", - "EventDefinition", - "Evidence", - "EvidenceVariable", - "ExampleScenario", - "ExplanationOfBenefit", - "FamilyMemberHistory", - "Flag", - "Goal", - "GraphDefinition", - "Group", - "GuidanceResponse", - "HealthcareService", - "ImagingStudy", - "Immunization", - "ImmunizationEvaluation", - "ImmunizationRecommendation", - "ImplementationGuide", - "InsurancePlan", - "Invoice", - "Library", - "Linkage", - "List", - "Location", - "Measure", - "MeasureReport", - "Media", - "Medication", - "MedicationAdministration", - "MedicationDispense", - "MedicationKnowledge", - "MedicationRequest", - "MedicationStatement", - "MedicinalProduct", - "MedicinalProductAuthorization", - "MedicinalProductContraindication", - "MedicinalProductIndication", - "MedicinalProductIngredient", - "MedicinalProductInteraction", - "MedicinalProductManufactured", - "MedicinalProductPackaged", - "MedicinalProductPharmaceutical", - "MedicinalProductUndesirableEffect", - "MessageDefinition", - "MessageHeader", - "MolecularSequence", - "NamingSystem", - "NutritionOrder", - "Observation", - "ObservationDefinition", - "OperationDefinition", - "OperationOutcome", - "Organization", - "OrganizationAffiliation", - "Parameters", - "Patient", - "PaymentNotice", - "PaymentReconciliation", - "Person", - "PlanDefinition", - "Practitioner", - "PractitionerRole", - "Procedure", - "Provenance", - "Questionnaire", - "QuestionnaireResponse", - "RelatedPerson", - "RequestGroup", - "ResearchDefinition", - "ResearchElementDefinition", - "ResearchStudy", - "ResearchSubject", - "RiskAssessment", - "RiskEvidenceSynthesis", - "Schedule", - "SearchParameter", - "ServiceRequest", - "Slot", - "Specimen", - "SpecimenDefinition", - "StructureDefinition", - "StructureMap", - "Subscription", - "Substance", - "SubstanceNucleicAcid", - "SubstancePolymer", - "SubstanceProtein", - "SubstanceReferenceInformation", - "SubstanceSourceMaterial", - "SubstanceSpecification", - "SupplyDelivery", - "SupplyRequest", - "Task", - "TerminologyCapabilities", - "TestReport", - "TestScript", - "ValueSet", - "VerificationResult", - "VisionPrescription" - ] - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - }, - "DomainResource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "enum": [ - "Resource", - "DomainResource", - "Account", - "ActivityDefinition", - "AdverseEvent", - "AllergyIntolerance", - "Appointment", - "AppointmentResponse", - "AuditEvent", - "Basic", - "Binary", - "BiologicallyDerivedProduct", - "BodyStructure", - "Bundle", - "CapabilityStatement", - "CarePlan", - "CareTeam", - "CatalogEntry", - "ChargeItem", - "ChargeItemDefinition", - "Claim", - "ClaimResponse", - "ClinicalImpression", - "CodeSystem", - "Communication", - "CommunicationRequest", - "CompartmentDefinition", - "Composition", - "ConceptMap", - "Condition", - "Consent", - "Contract", - "Coverage", - "CoverageEligibilityRequest", - "CoverageEligibilityResponse", - "DetectedIssue", - "Device", - "DeviceDefinition", - "DeviceMetric", - "DeviceRequest", - "DeviceUseStatement", - "DiagnosticReport", - "DocumentManifest", - "DocumentReference", - "EffectEvidenceSynthesis", - "Encounter", - "Endpoint", - "EnrollmentRequest", - "EnrollmentResponse", - "EpisodeOfCare", - "EventDefinition", - "Evidence", - "EvidenceVariable", - "ExampleScenario", - "ExplanationOfBenefit", - "FamilyMemberHistory", - "Flag", - "Goal", - "GraphDefinition", - "Group", - "GuidanceResponse", - "HealthcareService", - "ImagingStudy", - "Immunization", - "ImmunizationEvaluation", - "ImmunizationRecommendation", - "ImplementationGuide", - "InsurancePlan", - "Invoice", - "Library", - "Linkage", - "List", - "Location", - "Measure", - "MeasureReport", - "Media", - "Medication", - "MedicationAdministration", - "MedicationDispense", - "MedicationKnowledge", - "MedicationRequest", - "MedicationStatement", - "MedicinalProduct", - "MedicinalProductAuthorization", - "MedicinalProductContraindication", - "MedicinalProductIndication", - "MedicinalProductIngredient", - "MedicinalProductInteraction", - "MedicinalProductManufactured", - "MedicinalProductPackaged", - "MedicinalProductPharmaceutical", - "MedicinalProductUndesirableEffect", - "MessageDefinition", - "MessageHeader", - "MolecularSequence", - "NamingSystem", - "NutritionOrder", - "Observation", - "ObservationDefinition", - "OperationDefinition", - "OperationOutcome", - "Organization", - "OrganizationAffiliation", - "Parameters", - "Patient", - "PaymentNotice", - "PaymentReconciliation", - "Person", - "PlanDefinition", - "Practitioner", - "PractitionerRole", - "Procedure", - "Provenance", - "Questionnaire", - "QuestionnaireResponse", - "RelatedPerson", - "RequestGroup", - "ResearchDefinition", - "ResearchElementDefinition", - "ResearchStudy", - "ResearchSubject", - "RiskAssessment", - "RiskEvidenceSynthesis", - "Schedule", - "SearchParameter", - "ServiceRequest", - "Slot", - "Specimen", - "SpecimenDefinition", - "StructureDefinition", - "StructureMap", - "Subscription", - "Substance", - "SubstanceNucleicAcid", - "SubstancePolymer", - "SubstanceProtein", - "SubstanceReferenceInformation", - "SubstanceSourceMaterial", - "SubstanceSpecification", - "SupplyDelivery", - "SupplyRequest", - "Task", - "TerminologyCapabilities", - "TestReport", - "TestScript", - "ValueSet", - "VerificationResult", - "VisionPrescription" - ] - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - } - } - }, - "Immunization": { - "type": "object", - "required": [ - "status", - "vaccineCode", - "patient" - ], - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "example": "Immunization" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - }, - "identifier": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Identifier", - "description": "A unique identifier assigned to this immunization record." - } - }, - "status": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Indicates the current status of the immunization event." - }, - "statusReason": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates the reason the immunization event was not performed." - }, - "vaccineCode": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Vaccine that was administered or was to be administered." - }, - "patient": { - "$ref": "#/components/schemas/Reference", - "description": "The patient who either received or did not receive the immunization." - }, - "encounter": { - "$ref": "#/components/schemas/Reference", - "description": "The visit or admission or other contact between patient and health care provider the immunization was performed as part of." - }, - "occurrenceDateTime": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date vaccine administered or was to be administered." - }, - "occurrenceString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Date vaccine administered or was to be administered." - }, - "recorded": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event." - }, - "primarySource": { - "type": "boolean", - "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded." - }, - "reportOrigin": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The source of the data when the report of the immunization event is not based on information from the person who administered the vaccine." - }, - "location": { - "$ref": "#/components/schemas/Reference", - "description": "The service delivery location where the vaccine administration occurred." - }, - "manufacturer": { - "$ref": "#/components/schemas/Reference", - "description": "Name of vaccine manufacturer." - }, - "lotNumber": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Lot number of the vaccine product." - }, - "expirationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", - "description": "Date vaccine batch expires." - }, - "site": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Body site where vaccine was administered." - }, - "route": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The path by which the vaccine product is taken into the body." - }, - "doseQuantity": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The quantity of vaccine product that was administered." - }, - "performer": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Performer", - "description": "Indicates who performed the immunization event." - } - }, - "note": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Annotation", - "description": "Extra information about the immunization that is not conveyed by the other attributes." - } - }, - "reasonCode": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Reasons why the vaccine was administered." - } - }, - "reasonReference": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Reference", - "description": "Condition, Observation or DiagnosticReport that supports why the immunization was administered." - } - }, - "isSubpotent": { - "type": "boolean", - "description": "Indication if a dose is considered to be subpotent. By default, a dose should be considered to be potent." - }, - "subpotentReason": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Reason why a dose is considered to be subpotent." - } - }, - "education": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Education", - "description": "Educational material presented to the patient (or guardian) at the time of vaccine administration." - } - }, - "programEligibility": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates a patient's eligibility for a funding program." - } - }, - "fundingSource": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Indicates the source of the vaccine actually administered. This may be different than the patient eligibility (e.g. the patient may be eligible for a publically purchased vaccine but due to inventory issues, vaccine purchased with private funds was actually administered)." - }, - "reaction": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_Reaction", - "description": "Categorical data indicating that an adverse event is associated in time to an immunization." - } - }, - "protocolApplied": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Immunization_ProtocolApplied", - "description": "The protocol (set of recommendations) being followed by the provider who administered the dose." - } - } - }, - "example": { - "resourceType": "Immunization", - "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", - "contained": [ - { - "resourceType": "Practitioner", - "id": "Pract1", - "name": [ - { - "family": "Owl", - "given": [ - "Barney" - ] - } - ] - }, - { - "resourceType": "Patient", - "id": "Pat1", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449310475" - } - ], - "name": [ - { - "family": "Owler", - "given": [ - "Ozzie" - ] - } - ], - "gender": "unknown", - "birthDate": "1965-02-28", - "address": [ - { - "postalCode": "ZZ99 3CZ" - } - ] - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "#Pat1" - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "urn:iso:std:iso:3166", - "value": "GB" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "reference": "#Pract1" - } - }, - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "N2N9I" - } - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - } - }, - "Bundle": { - "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", - "type": "object", - "required": [ - "resourceType", - "type" - ], - "properties": { - "resourceType": { - "description": "FHIR resource type. Always `Bundle`.", - "type": "string", - "example": "Bundle" - }, - "type": { - "description": "Indicates how the bundle is intended to be used. Always `searchset`.", - "type": "string", - "example": "searchset" - }, - "link": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Link", - "description": "A series of links that provide context to this bundle." - } - }, - "entry": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Entry", - "description": "An entry in a bundle resource - will either contain a resource or information about a resource (transactions and history only)." - } - }, - "total": { - "type": "integer", - "format": "int32", - "description": "If a set of search matches, this is the total number of entries of type 'match' across all pages in the search. It does not include search.mode = 'include' or 'outcome' entries and it does not provide a count of the number of entries in the Bundle." - } - }, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "link": [ - { - "relation": "self", - "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=COVID19&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9449306206" - } - ], - "entry": [ - { - "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", - "resource": { - "resourceType": "Immunization", - "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", - "meta": { - "versionId": "1" - }, - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "1303503001", - "display": "Administration of RSV (respiratory syncytial virus) vaccine" - } - ] - } - } - ], - "identifier": [ - { - "use": "official", - "system": "https://supplierABC/identifiers/vacc", - "value": "e2154d29-1ead-4830-a513-0d59705078fa" - } - ], - "status": "completed", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "42605811000001109", - "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" - } - ] - }, - "patient": { - "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "type": "Patient", - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449306206" - } - }, - "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", - "recorded": "2021-02-07T13:28:17.271000+00:00", - "primarySource": true, - "location": { - "identifier": { - "system": "urn:iso:std:iso:3166", - "value": "GB" - } - }, - "manufacturer": { - "display": "AstraZeneca Ltd" - }, - "lotNumber": "4120Z001", - "expirationDate": "2021-07-02", - "site": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "368208006", - "display": "Left upper arm structure (body structure)" - } - ] - }, - "route": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "78421000", - "display": "Intramuscular route (qualifier value)" - } - ] - }, - "doseQuantity": { - "value": 0.5, - "unit": "milliliter", - "system": "http://unitsofmeasure.org", - "code": "ml" - }, - "performer": [ - { - "actor": { - "type": "Organization", - "identifier": { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "B0C4P" - }, - "display": "UNIVERSITY HOSPITAL OF WALES" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "443684005", - "display": "Disease outbreak (event)" - } - ] - } - ], - "protocolApplied": [ - { - "targetDisease": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "55735004", - "display": "Respiratory syncytial virus infection (disorder)" - } - ] - } - ], - "doseNumberPositiveInt": 1 - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", - "resource": { - "resourceType": "Patient", - "id": "9449306206", - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9449306206" - } - ], - "birthDate": "2014-03-25" - }, - "search": { - "mode": "include" - } - } - ], - "total": 1 - } - }, - "OperationOutcome": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/Narrative", - "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." - }, - "contained": { - "type": "array", - "items": { - "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", - "type": "object", - "discriminator": { - "propertyName": "resourceType" - }, - "properties": { - "resourceType": { - "type": "string", - "example": "OperationOutcome" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "issue": { - "type": "array", - "items": { - "type": "object", - "properties": { - "severity": { - "type": "string" - }, - "code": { - "type": "string" - }, - "details": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "items": { - "type": "object", - "properties": { - "system": { - "type": "string" - }, - "code": { - "type": "string" - } - } - } - } - } - }, - "diagnostics": { - "type": "string" - } - } - } - } - }, - "required": [ - "resourceType" - ] - } - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - }, - "issue": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OperationOutcome_Issue", - "description": "An error, warning, or information message that results from a system action." - }, - "minItems": 1 - } - }, - "required": [ - "issue" - ], - "example": { - "resourceType": "OperationOutcome", - "meta": { - "versionId": "BnpJOa5-Sb", - "lastUpdated": "2021-04-12T14:34:36.061-05:00", - "source": "BCL3d5NERb", - "profile": [ - "xSempdez3Y" - ], - "security": [ - { - "system": "tczS7uP8XL", - "version": "IXKbCw05qO", - "code": "NvDP1hL64Y", - "display": "_r1z5oJld1", - "userSelected": true - } - ], - "tag": [ - { - "system": "2qqXHsE1Mx", - "version": "lybFyQ1tBj", - "code": "Q9w075fYd3", - "display": "Nm2QqbYibP", - "userSelected": true - }, - { - "code": "ibm/complete-mock" - } - ] - }, - "implicitRules": "l8KHk6qOt4", - "language": "en-US", - "text": { - "status": "additional", - "div": "
" - }, - "issue": [ - { - "severity": "warning", - "code": "business-rule", - "details": { - "coding": [ - { - "system": "eQgFofRHmJ", - "version": "T524HDk5Za", - "code": "tC_7iQg31j", - "display": "s0bLc4W5KE", - "userSelected": true - } - ], - "text": "BfBVppHmsh" - }, - "diagnostics": "EcvPDGbK0q", - "location": [ - "GK5ihTmfe6" - ], - "expression": [ - "Uidx_swV4Z" - ] - } - ] - } - }, - "Bundle_Entry": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "link": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bundle_Link", - "description": "A series of links that provide context to this entry." - } - }, - "fullUrl": { - "type": "string", - "pattern": "\\S*", - "description": "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource - i.e. if the fullUrl is not a urn:uuid, the URL shall be version-independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified." - }, - "resource": { - "description": "The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type." - }, - "resourceType": { - "type": "string", - "example": "Immunization" - }, - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." - }, - "meta": { - "$ref": "#/components/schemas/Meta", - "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." - }, - "implicitRules": { - "type": "string", - "pattern": "\\S*", - "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The base language in which the resource is written." - }, - "search": { - "$ref": "#/components/schemas/Bundle_Entry_Search", - "description": "Information about the search process that lead to the creation of this entry." - }, - "request": { - "$ref": "#/components/schemas/Bundle_Entry_Request", - "description": "Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry." - }, - "response": { - "$ref": "#/components/schemas/Bundle_Entry_Response", - "description": "Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history." - } - } - } - ] - }, - "Bundle_Entry_Response": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The status code returned by processing this entry. The status SHALL start with a 3 digit HTTP code (e.g. 404) and may contain the standard HTTP description associated with the status code." - }, - "location": { - "type": "string", - "pattern": "\\S*", - "description": "The location header created by processing this operation, populated if the operation returns a location." - }, - "etag": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The Etag for the resource, if the operation for the entry produced a versioned resource (see [Resource Metadata and Versioning](http.html#versioning) and [Managing Resource Contention](http.html#concurrency))." - }, - "lastModified": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "The date/time that the resource was modified on the server." - }, - "outcome": { - "$ref": "#/components/schemas/Resource", - "description": "An OperationOutcome containing hints and warnings produced as part of processing this entry in a batch or transaction." - } - }, - "required": [ - "status" - ] - } - ] - }, - "Bundle_Entry_Request": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "method": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "In a transaction or batch, this is the HTTP action to be executed for this entry. In a history bundle, this indicates the HTTP action that occurred." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "The URL for this entry, relative to the root (the address to which the request is posted)." - }, - "ifNoneMatch": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "If the ETag values match, return a 304 Not Modified status. See the API documentation for [\"Conditional Read\"](http.html#cread)." - }, - "ifModifiedSince": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "Only perform the operation if the last updated date matches. See the API documentation for [\"Conditional Read\"](http.html#cread)." - }, - "ifMatch": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Only perform the operation if the Etag value matches. For more information, see the API section [\"Managing Resource Contention\"](http.html#concurrency)." - }, - "ifNoneExist": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Instruct the server not to perform the create if a specified resource already exists. For further information, see the API documentation for [\"Conditional Create\"](http.html#ccreate). This is just the query portion of the URL - what follows the \"?\" (not including the \"?\")." - } - }, - "required": [ - "method", - "url" - ] - } - ] - }, - "Bundle_Entry_Search": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "mode": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Why this entry is in the result set - whether it's included as a match or because of an _include requirement, or to convey information or warning information about the search process." - }, - "score": { - "type": "number", - "description": "When searching, the server's search ranking score for the entry." - } - } - } - ] - }, - "Bundle_Link": { - "allOf": [ - { - "type": "object", - "properties": { - "relation": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1)." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "The reference details for the link." - } - }, - "required": [ - "relation", - "url" - ] - } - ] - }, - "Immunization_ProtocolApplied": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "series": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "One possible path to achieve presumed immunity against a disease - within the context of an authority." - }, - "authority": { - "$ref": "#/components/schemas/Reference", - "description": "Indicates the authority who published the protocol (e.g. ACIP) that is being followed." - }, - "targetDisease": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "The vaccine preventable disease the dose is being administered against." - } - }, - "doseNumberPositiveInt": { - "type": "integer", - "format": "int32", - "description": "Nominal position in a series." - }, - "doseNumberString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Nominal position in a series." - }, - "seriesDosesPositiveInt": { - "type": "integer", - "format": "int32", - "description": "The recommended number of doses to achieve immunity." - }, - "seriesDosesString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The recommended number of doses to achieve immunity." - } - } - } - ] - }, - "Immunization_Reaction": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "date": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date of reaction to the immunization." - }, - "detail": { - "$ref": "#/components/schemas/Reference", - "description": "Details of the reaction." - }, - "reported": { - "type": "boolean", - "description": "Self-reported indicator." - } - } - } - ] - }, - "Immunization_Education": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "documentType": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Identifier of the material presented to the patient." - }, - "reference": { - "type": "string", - "pattern": "\\S*", - "description": "Reference pointer to the educational material given to the patient if the information was on line." - }, - "publicationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date the educational material was published." - }, - "presentationDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Date the educational material was given to the patient." - } - } - } - ] - }, - "Immunization_Performer": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "function": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Describes the type of performance (e.g. ordering provider, administering provider, etc.)." - }, - "actor": { - "$ref": "#/components/schemas/Reference", - "description": "The practitioner or organization who performed the action." - } - }, - "required": [ - "actor" - ] - } - ] - }, - "OperationOutcome_Issue": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "severity": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Indicates whether the issue indicates a variation from successful processing." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." - }, - "details": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Additional details about the error. This may be a text description of the error or a system code that identifies the error." - }, - "diagnostics": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Additional diagnostic information about the issue." - }, - "location": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "This element is deprecated because it is XML specific. It is replaced by issue.expression, which is format independent, and simpler to parse. \n\nFor resource issues, this will be a simple XPath limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised. For HTTP errors, will be \"http.\" + the parameter name." - } - }, - "expression": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A [simple subset of FHIRPath](fhirpath.html#simple) limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised." - } - } - }, - "required": [ - "severity", - "code" - ] - } - ] - }, - "Element": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - }, - "example": [ - { - "url": "http://example.com", - "valueString": "text value" - } - ] - } - } - }, - "BackboneElement": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "modifierExtension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." - } - } - } - }, - "Address": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The purpose of this address." - }, - "type": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Distinguishes between physical addresses (those you can visit) and mailing addresses (e.g. PO Boxes and care-of addresses). Most addresses are both." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Specifies the entire address as it should be displayed e.g. on a postal label. This may be provided instead of or as well as the specific parts." - }, - "line": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "This component contains the house number, apartment number, street name, street direction, P.O. Box number, delivery hints, and similar address information." - } - }, - "city": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of the city, town, suburb, village or other community or delivery center." - }, - "district": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of the administrative area (county)." - }, - "state": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Sub-unit of a country with limited sovereignty in a federally organized country. A code may be used if codes are in common use (e.g. US 2 letter state codes)." - }, - "postalCode": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A postal code designating a region defined by the postal service." - }, - "country": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Country - a nation as commonly understood or generally accepted." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period when address was/is in use." - } - } - } - ] - }, - "Age": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Annotation": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "authorReference": { - "$ref": "#/components/schemas/Reference", - "description": "The individual responsible for making the annotation." - }, - "authorString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The individual responsible for making the annotation." - }, - "time": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Indicates when this particular annotation was made." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The text of the annotation in markdown format." - } - }, - "required": [ - "text" - ] - } - ] - }, - "Attachment": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate." - }, - "language": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The human language of the content. The value can be any valid value according to BCP 47." - }, - "data": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The actual data of the attachment - a sequence of bytes, base64 encoded." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "A location where the data can be accessed." - }, - "size": { - "type": "integer", - "format": "int32", - "description": "The number of bytes of data that make up this attachment (before base64 encoding, if that is done)." - }, - "hash": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The calculated hash of the data using SHA-1. Represented using base64." - }, - "title": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A label or set of text to display in place of the data." - }, - "creation": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The date that the attachment was first created." - } - } - } - ] - }, - "CodeableConcept": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "coding": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "A reference to a code defined by a terminology system." - } - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user." - } - } - }, - "Coding": { - "type": "object", - "properties": { - "system": { - "type": "string", - "pattern": "\\S*", - "description": "The identification of the code system that defines the meaning of the symbol in the code." - }, - "version": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination)." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A representation of the meaning of the code in the system, following the rules of the system." - }, - "userSelected": { - "type": "boolean", - "description": "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays)." - } - } - }, - "ContactPoint": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "system": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Telecommunications form for contact point - what communications system is required to make use of the contact." - }, - "value": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The actual contact point details, in a form that is meaningful to the designated communication system (i.e. phone number or email address)." - }, - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the purpose for the contact point." - }, - "rank": { - "type": "integer", - "format": "int32", - "description": "Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period when the contact point was/is in use." - } - } - } - ] - }, - "Count": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Distance": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Duration": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "HumanName": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Identifies the purpose for this name." - }, - "text": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts." - }, - "family": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father." - }, - "given": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Given name." - } - }, - "prefix": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name." - } - }, - "suffix": { - "type": "array", - "items": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name." - } - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Indicates the period of time when this name was valid for the named person." - } - } - } - ] - }, - "Identifier": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "use": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The purpose of this identifier." - }, - "type": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A coded type for the identifier that can be used to determine which identifier to use for a specific purpose." - }, - "system": { - "type": "string", - "pattern": "\\S*", - "description": "Establishes the namespace for the value - that is, a URL that describes a set values that are unique." - }, - "value": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The portion of the identifier typically relevant to the user and which is unique within the context of the system." - }, - "period": { - "$ref": "#/components/schemas/Period", - "description": "Time period during which identifier is/was valid for use." - }, - "assigner": { - "$ref": "#/components/schemas/Reference", - "description": "Organization that issued/manages the identifier.", - "example": { - "reference": "Organization/123", - "type": "Organization", - "display": "The Assigning Organization" - } - } - } - }, - "Money": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "description": "Numerical value (with implicit precision)." - }, - "currency": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "ISO 4217 Currency Code." - } - } - } - ] - }, - "MoneyQuantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Period": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "start": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The start of the period. The boundary is inclusive." - }, - "end": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time." - } - } - } - ] - }, - "Quantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "description": "The value of the measured amount. The value includes an implicit precision in the presentation of the value." - }, - "comparator": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "How the value should be understood and represented - whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is \"<\" , then the real value is < stated value." - }, - "unit": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A human-readable form of the unit." - }, - "system": { - "type": "string", - "pattern": "\\S*", - "description": "The identification of the system that provides the coded form of the unit." - }, - "code": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A computer processable form of the unit in some unit representation system." - } - } - } - ] - }, - "Range": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "low": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The low limit. The boundary is inclusive." - }, - "high": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The high limit. The boundary is inclusive." - } - } - } - ] - }, - "Ratio": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "numerator": { - "$ref": "#/components/schemas/Quantity", - "description": "The value of the numerator." - }, - "denominator": { - "$ref": "#/components/schemas/Quantity", - "description": "The value of the denominator." - } - } - } - ] - }, - "Reference": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "reference": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources." - }, - "type": { - "type": "string", - "pattern": "\\S*", - "description": "The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent.\n\nThe type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. \"Patient\" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources)." - }, - "identifier": { - "$ref": "#/components/schemas/Identifier", - "description": "An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Plain text narrative that identifies the resource in addition to the resource reference." - } - } - }, - "SampledData": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "origin": { - "$ref": "#/components/schemas/SimpleQuantity", - "description": "The base quantity that a measured value of zero represents. In addition, this provides the units of the entire measurement series." - }, - "period": { - "type": "number", - "description": "The length of time between sampling times, measured in milliseconds." - }, - "factor": { - "type": "number", - "description": "A correction factor that is applied to the sampled data points before they are added to the origin." - }, - "lowerLimit": { - "type": "number", - "description": "The lower limit of detection of the measured points. This is needed if any of the data points have the value \"L\" (lower than detection limit)." - }, - "upperLimit": { - "type": "number", - "description": "The upper limit of detection of the measured points. This is needed if any of the data points have the value \"U\" (higher than detection limit)." - }, - "dimensions": { - "type": "integer", - "format": "int32", - "description": "The number of sample points at each time point. If this value is greater than one, then the dimensions will be interlaced - all the sample points for a point in time will be recorded at once." - }, - "data": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A series of data points which are decimal values separated by a single space (character u20). The special values \"E\" (error), \"L\" (below detection limit) and \"U\" (above detection limit) can also be used in place of a decimal value." - } - }, - "required": [ - "origin", - "period", - "dimensions" - ] - } - ] - }, - "SimpleQuantity": { - "allOf": [ - { - "$ref": "#/components/schemas/Quantity" - }, - { - "type": "object", - "properties": {} - } - ] - }, - "Signature": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "type": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "An indication of the reason that the entity signed this document. This may be explicitly included as part of the signature information and can be used when determining accountability for various actions concerning the document." - }, - "minItems": 1 - }, - "when": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the digital signature was signed." - }, - "who": { - "$ref": "#/components/schemas/Reference", - "description": "A reference to an application-usable description of the identity that signed (e.g. the signature used their private key)." - }, - "onBehalfOf": { - "$ref": "#/components/schemas/Reference", - "description": "A reference to an application-usable description of the identity that is represented by the signature." - }, - "targetFormat": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A mime type that indicates the technical format of the target resources signed by the signature." - }, - "sigFormat": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "A mime type that indicates the technical format of the signature. Important mime types are application/signature+xml for X ML DigSig, application/jose for JWS, and image/* for a graphical image of a signature, etc." - }, - "data": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "The base64 encoding of the Signature content. When signature is not recorded electronically this element would be empty." - } - }, - "required": [ - "type", - "when", - "who" - ] - } - ] - }, - "Timing": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "event": { - "type": "array", - "items": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Identifies specific times when the event occurs." - } - }, - "repeat": { - "$ref": "#/components/schemas/Timing_Repeat", - "description": "A set of rules that describe when the event is scheduled." - }, - "code": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A code for the timing schedule (or just text in code.text). Some codes such as BID are ubiquitous, but many institutions define their own additional codes. If a code is provided, the code is understood to be a complete statement of whatever is specified in the structured timing data, and either the code or the data may be used to interpret the Timing, with the exception that .repeat.bounds still applies over the code (and is not contained in the code)." - } - } - } - ] - }, - "Timing_Repeat": { - "allOf": [ - { - "$ref": "#/components/schemas/BackboneElement" - }, - { - "type": "object", - "properties": { - "boundsDuration": { - "$ref": "#/components/schemas/Duration", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "boundsRange": { - "$ref": "#/components/schemas/Range", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "boundsPeriod": { - "$ref": "#/components/schemas/Period", - "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." - }, - "count": { - "type": "integer", - "format": "int32", - "description": "A total count of the desired number of repetitions across the duration of the entire timing specification. If countMax is present, this element indicates the lower bound of the allowed range of count values." - }, - "countMax": { - "type": "integer", - "format": "int32", - "description": "If present, indicates that the count is a range - so to perform the action between [count] and [countMax] times." - }, - "duration": { - "type": "number", - "description": "How long this thing happens for when it happens. If durationMax is present, this element indicates the lower bound of the allowed range of the duration." - }, - "durationMax": { - "type": "number", - "description": "If present, indicates that the duration is a range - so to perform the action between [duration] and [durationMax] time length." - }, - "durationUnit": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The units of time for the duration, in UCUM units." - }, - "frequency": { - "type": "integer", - "format": "int32", - "description": "The number of times to repeat the action within the specified period. If frequencyMax is present, this element indicates the lower bound of the allowed range of the frequency." - }, - "frequencyMax": { - "type": "integer", - "format": "int32", - "description": "If present, indicates that the frequency is a range - so to repeat between [frequency] and [frequencyMax] times within the period or period range." - }, - "period": { - "type": "number", - "description": "Indicates the duration of time over which repetitions are to occur; e.g. to express \"3 times per day\", 3 would be the frequency and \"1 day\" would be the period. If periodMax is present, this element indicates the lower bound of the allowed range of the period length." - }, - "periodMax": { - "type": "number", - "description": "If present, indicates that the period is a range from [period] to [periodMax], allowing expressing concepts such as \"do this once every 3-5 days." - }, - "periodUnit": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The units of time for the period in UCUM units." - }, - "dayOfWeek": { - "type": "array", - "items": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "If one or more days of week is provided, then the action happens only on the specified day(s)." - } - }, - "timeOfDay": { - "type": "array", - "items": { - "type": "string", - "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", - "description": "Specified time of day for action to take place." - } - }, - "when": { - "type": "array", - "items": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "An approximate time period during the day, potentially linked to an event of daily living that indicates when the action should occur." - } - }, - "offset": { - "type": "integer", - "format": "int32", - "description": "The number of minutes from the event. If the event code does not indicate whether the minutes is before or after the event, then the offset is assumed to be after the event." - } - } - } - ] - }, - "ContactDetail": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "The name of an individual to contact." - }, - "telecom": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ContactPoint", - "description": "The contact details for the individual (if a name was provided) or the organization." - } - } - } - } - ] - }, - "RelatedArtifact": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The type of relationship to the related artifact." - }, - "label": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A short label that can be used to reference the citation from elsewhere in the containing artifact, such as a footnote index." - }, - "display": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A brief description of the document or knowledge resource being referenced, suitable for display to a consumer." - }, - "citation": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "A bibliographic citation for the related artifact. This text SHOULD be formatted according to an accepted citation format." - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "A url for the artifact that can be followed to access the actual content." - }, - "document": { - "$ref": "#/components/schemas/Attachment", - "description": "The document being referenced, represented as an attachment. This is exclusive with the resource element." - }, - "resource": { - "type": "string", - "pattern": "\\S*", - "description": "The related resource, such as a library, value set, profile, or other knowledge resource." - } - }, - "required": [ - "type" - ] - } - ] - }, - "UsageContext": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "code": { - "$ref": "#/components/schemas/Coding", - "description": "A code that identifies the type of context being specified by this usage context." - }, - "valueCodeableConcept": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueQuantity": { - "$ref": "#/components/schemas/Quantity", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueRange": { - "$ref": "#/components/schemas/Range", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - }, - "valueReference": { - "$ref": "#/components/schemas/Reference", - "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." - } - }, - "required": [ - "code" - ] - } - ] - }, - "Meta": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "versionId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." - }, - "lastUpdated": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "When the resource last changed - e.g. when the version changed." - }, - "source": { - "type": "string", - "pattern": "\\S*", - "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." - }, - "profile": { - "type": "array", - "items": { - "type": "string", - "pattern": "\\S*", - "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." - } - }, - "security": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." - } - }, - "tag": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Coding", - "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." - } - } - }, - "required": [ - "versionId" - ] - } - ] - }, - "Narrative": { - "allOf": [ - { - "$ref": "#/components/schemas/Element" - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "The status of the narrative - whether it's entirely generated (from just the defined data or the extensions too), or whether a human authored it and it may contain additional data." - }, - "div": { - "type": "string", - "description": "The actual narrative content, a stripped down version of XHTML." - } - }, - "required": [ - "status", - "div" - ] - } - ] - }, - "Extension": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - }, - "extension": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Extension", - "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." - } - }, - "url": { - "type": "string", - "pattern": "\\S*", - "description": "Source of the definition for the extension code - a logical name or a URL." - }, - "valueBase64Binary": { - "type": "string", - "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueBoolean": { - "type": "boolean", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCanonical": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCode": { - "type": "string", - "pattern": "[^\\s]+(\\s[^\\s]+)*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDate": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDateTime": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDecimal": { - "type": "number", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueId": { - "type": "string", - "pattern": "[A-Za-z0-9\\-\\.]{1,64}", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueInstant": { - "type": "string", - "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueInteger": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMarkdown": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueOid": { - "type": "string", - "pattern": "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valuePositiveInt": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueString": { - "type": "string", - "pattern": "[ \\r\\n\\t\\S]+", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueTime": { - "type": "string", - "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUnsignedInt": { - "type": "integer", - "format": "int32", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUri": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUrl": { - "type": "string", - "pattern": "\\S*", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUuid": { - "type": "string", - "pattern": "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAddress": { - "$ref": "#/components/schemas/Address", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAge": { - "$ref": "#/components/schemas/Age", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAnnotation": { - "$ref": "#/components/schemas/Annotation", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueAttachment": { - "$ref": "#/components/schemas/Attachment", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCodeableConcept": { - "$ref": "#/components/schemas/CodeableConcept", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCoding": { - "$ref": "#/components/schemas/Coding", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueContactPoint": { - "$ref": "#/components/schemas/ContactPoint", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueCount": { - "$ref": "#/components/schemas/Count", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDistance": { - "$ref": "#/components/schemas/Distance", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueDuration": { - "$ref": "#/components/schemas/Duration", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueHumanName": { - "$ref": "#/components/schemas/HumanName", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueIdentifier": { - "$ref": "#/components/schemas/Identifier", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMoney": { - "$ref": "#/components/schemas/Money", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valuePeriod": { - "$ref": "#/components/schemas/Period", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueQuantity": { - "$ref": "#/components/schemas/Quantity", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRange": { - "$ref": "#/components/schemas/Range", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRatio": { - "$ref": "#/components/schemas/Ratio", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueReference": { - "$ref": "#/components/schemas/Reference", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueSampledData": { - "$ref": "#/components/schemas/SampledData", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueSignature": { - "$ref": "#/components/schemas/Signature", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueTiming": { - "$ref": "#/components/schemas/Timing", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueContactDetail": { - "$ref": "#/components/schemas/ContactDetail", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueRelatedArtifact": { - "$ref": "#/components/schemas/RelatedArtifact", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueUsageContext": { - "$ref": "#/components/schemas/UsageContext", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - }, - "valueMeta": { - "$ref": "#/components/schemas/Meta", - "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." - } - }, - "required": [ - "url" - ] - } - } - } - } \ No newline at end of file +{ + "openapi": "3.0.0", + "info": { + "title": "Immunization-fhir-api", + "version": "Computed and injected at build time by `scripts/set_version.py`", + "description": "## Overview\n \nUse this API to access a patient's immunisation record. It is part of the [Vaccinations Data Flow Management](https://digital.nhs.uk/services/vaccinations-data-flow-management). It is intended to extend and replace [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) and existing [Vaccination](https://digital.nhs.uk/developer/api-catalogue/vaccination) flows. \n\nYou can use this API to:\n \n- create and record a patient immunisation \n- search for a patient's immunisation records\n- get the details of an immunisation record\n- update an immunisation record \n- identify an immunisation record as entered in error \n \nYou cannot use this API to:\n \n- retrieve the immunisation record of multiple patients at once \n- record or update a patient's demographic details\n \nYou can create, read, update and delete events for the following vaccination types:\n \n- coronavirus (`COVID19`)\n- influenza (`FLU`)\n- measles, mumps and rubella (`MMR`)\n- human papillomavirus (`HPV`)\n- tetanus, diphtheria and polio (`3IN1`)\n- meningococcal infectious disease (`MENACWY`)\n- respiratory syncytial virus infection (`RSV`)\n \n### Data availability, timing and quality\n\nThis is a real-time service, constrained by the time taken for providers to transfer vaccination events. In most cases, a record will become available within 48 hours of the immunisation event.\n\nThe API search interaction will only return immunisation records based on a traced NHS number. Other interactions require the use of the immunisation ID assigned by the API to interact with individual records for read, update and delete.\n\nThe vaccination events for all disease types are limited to vaccinations administered on behalf of NHS England.\n\nThere is a limited scope of data validation upon receipt of the data. Whilst the data is generally of a good, reliable quality, consumers must be aware that data is shared as received, and users should consider the risk of potential absences or inaccuracies of the data. \n\n \n## Who can use this API \nThis API can only be used where there is a commercial, legal and clinical basis to do so. Make sure you have a valid use case before you go too far with your development.\n\nYou must demonstrate you have a valid use case as part of digital onboarding. \n\nYou must do this before you can go live (see [Onboarding](https://digital.nhs.uk/developer/api-catalogue/immunisation-fhir-api#overview--onboarding) below).\n\n### Who can access immunisation event records\n \nHealth and care organisations in England can access immunisation event records.\n\nLegitimate direct care examples include NHS organisations delivering healthcare, local authorities delivering care, third sector and private sector health and care organisations, and developers delivering systems to health and care organisations.\n\n## API status and roadmap\nThe current roadmap includes enhancements to support all vaccines covered by the national Section 7a immunisation programme.\n \nThis API is in [Beta](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#statuses) for the following vaccine types:\n \n- respiratory syncytial virus (RSV)\n- influenza\n- human papillomavirus (HPV)\n\nThis API is in development for the following vaccine types:\n \n- Measles, Mumps and Rubella (MMR)\n- MenACWY\n- diphtheria, tetanus and polio (3-in-1)\n \nThis API will be configured to support all other section 7a vaccines from early 2026 onwards, adding support for the following vaccine types:\n \n- pneumococcal\n- shingles\n- pertussis\n- coronavirus (COVID-19) vaccinations\n- MMRV\n- diphtheria / tetanus / acellular pertussis / inactivated polio vaccine / haemophilus influenzae type b / hepatitis B (6-in-1)\n- rotavirus\n- meningococcal (MenB)\n- diphtheria / tetanus / acellular pertussis / inactivated polio vaccine (4-in-1)\n- haemophilus influenzae type b / meningococcal group C (Hib/MenC)\n- Bacillus Calmette-Guérin (BCG)\n- hepatitis B\n \nTo suggest new features, comment, or if you have any other queries, [contact us](https://digital.nhs.uk/developer/help-and-support).\n \n## Service level\nThis API will be a platinum service, meaning it is operational and supported 24 x 7 x 365.\n \nFor more details, see [service levels](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#service-levels).\n \n## Technology\n \nThis API is [RESTful](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#basic-rest).\n \nIt conforms to the [FHIR](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) global standard for health care data exchange, specifically to [FHIR R4 (v4.0.1)](https://hl7.org/fhir/r4/), except that it does not support the [capabilities](http://hl7.org/fhir/R4/http.html#capabilities) interaction.\n \nIt includes some country-specific FHIR extensions, which conform to [FHIR UK Core](https://digital.nhs.uk/services/fhir-uk-core), specifically [fhir.r4.ukcore.stu2](https://simplifier.net/packages/fhir.r4.ukcore.stu2).\n \nYou do not need to know much about FHIR to use this API - FHIR APIs are just RESTful APIs that follow specific rules.\nIn particular:\n- resource names are capitalised and singular, and use US spellings, for example `/Immunization` not `/immunisations`\n- array names are singular, for example `entry` not `entries` for address lines\n- data items that are country-specific and thus not included in the FHIR global base resources are usually wrapped in an `extension` object\n \nThere are [libraries and SDKs available](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) to help with FHIR API integration.\n \n## Network access\nThis API is available on the internet and, indirectly, on the [Health and Social Care Network (HSCN)](https://digital.nhs.uk/services/health-and-social-care-network).\n \nFor more details see [Network access for APIs](https://digital.nhs.uk/developer/guides-and-documentation/network-access-for-apis).\n \n## Security and authorisation\n \nThis API currently has a single access mode: [application-restricted access](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation#application-restricted-apis), meaning we authenticate the calling application but not the end user.\n \nTo use this access mode, use the following security pattern:\n- [Application-restricted RESTful API - signed JWT authentication](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication) \n\n## Errors\nWe use standard HTTP status codes to show whether an API request succeeded or not. They are usually in the range:\n\n* 200 to 299 if it succeeded, including code 202 if it was accepted by an API that needs to wait for further action\n* 400 to 499 if it failed because of a client error by your application\n* 500 to 599 if it failed because of an error on our server\n\nErrors specific to each API are shown in the Endpoints section, under Response. See our [reference guide](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#http-status-codes) for more on errors.\n\n## Open source\n\nYou might find the following [open source](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#open-source) resources useful:\n\n| Resource | Description | Links |\n|---------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------|\n| Immunisation FHIR API | Source code for the API proxy, sandbox and specification. | [GitHub repo](https://github.dev/NHSDigital/immunisation-fhir-api/) |\n| FHIR libraries and SDKs | Various open source libraries for integrating with FHIR APIs. | [FHIR libraries and SDKs](https://digital.nhs.uk/developer/guides-and-documentation/api-technologies-at-nhs-digital#fhir-libraries-and-sdks) |\n| nhs-number | Python package containing utilities for NHS numbers including validity checks, normalisation and generation. | [GitHub repo](https://github.com/uk-fci/nhs-number) \\| [Python Package index](https://pypi.org/project/nhs-number/) \\| [Docs](https://nhs-number.uk-fci.tech/) |\n\n## Environments and Testing\n| Environment | Base URL |\n| ----------------- | --------------------------------------------------------------------- |\n| Sandbox | `https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n| Integration | `https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` | \n| Production | `https://api.service.nhs.uk/immunisation-fhir-api/FHIR/R4` |\n\n### Sandbox testing\nOur [sandbox environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#sandbox-testing):\n* is for early developer testing\n* only covers a limited set of scenarios\n* is stateless, so does not actually persist any updates\n* is open access, so does not allow you to test authorisation\n\nFor details of sandbox test scenarios, or to try out the sandbox using our 'Try this API' feature, see the documentation for each endpoint.\n\n### Integration testing\nOur [integration test environment](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing):\n* is for formal integration testing\n* is stateful, so persists updates\n* includes authorisation, with options for application-restricted access \n\nFor read-only testing, we will provide an Immunisation records test pack soon.\n\nTo test creating, updating and deleting patient vaccination events, you must set up your own test data.\n\nFor more details see [integration testing with our RESTful APIs](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing-with-our-restful-apis).\n\n## Onboarding\n \nYou need to get your software approved by us before it can go live with this API.\nWe call this onboarding.\nThe onboarding process can sometimes be quite long, so it's worth planning well ahead.\n\nThis API is currently in private Beta, but we expect to open it to new consumers soon. As part of this process, you need to demonstrate that you can manage risks and that your software conforms technically with the requirements for this API. Information on this page might impact the design of your software.\n\nTo understand how our online digital onboarding process works, see [digital onboarding](https://digital.nhs.uk/developer/guides-and-documentation/digital-onboarding#using-the-digital-onboarding-portal).\n\n## Related APIs\n\nThe following APIs are related to this API:\n\n### Immunisation History - FHIR API\nUse the [Immunisation History - FHIR API](https://digital.nhs.uk/developer/api-catalogue/immunisation-history-fhir) if you want to access vaccination records that are not yet available on this API.\n\n## Contact us\nFor help and support connecting to our APIs and to join our developer community, see [Help and support building healthcare software](https://digital.nhs.uk/developer/help-and-support). \n" + }, + "servers": [ + { + "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", + "description": "Sandbox Server" + }, + { + "url": "https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4", + "description": "Integration Server" + } + ], + "paths": { + "/Immunization": { + "post": { + "summary": "Record a vaccination given to a patient", + "operationId": "createImmunization", + "description": "## Overview\nUse this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the disease types enabled in this interaction and represented by the correct SNOMED concept(s) for that disease type. A [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) is provided for supported disease types. \nYou must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n | Record a vaccination event | Valid request as per schema | HTTP Status 201 with immunisation id in response header (location) |\n| Bad Request (missing/invalid required element in request body) | Didn't pass `resourceType` in request body | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + } + ], + "requestBody": { + "content": { + "application/fhir+json": { + "schema": { + "description": "A FHIR Immunization resource.", + "type": "object", + "required": [ + "resourceType", + "contained", + "extension", + "identifier", + "status", + "vaccineCode", + "patient", + "occurrence[X]", + "recorded", + "primarySource", + "location", + "performer", + "protocolApplied" + ], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Immunization`.", + "type": "string", + "example": "Immunization" + }, + "meta": { + "type": "object", + "description": "Metadata about the resource.", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed.", + "example": "2017-01-01T00:00:00Z" + }, + "source": { + "type": "string", + "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + } + }, + "contained": { + "type": "array", + "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Practitioner`.", + "example": "Practitioner" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact" + }, + "name": { + "type": "array", + "description": "The name(s) associated with the practitioner", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Given names (not always 'first').", + "items": { + "type": "string" + } + } + } + } + } + }, + "required": ["resourceType", "id"] + }, + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Patient`.", + "example": "Patient" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact", + "example": "#Pat1" + }, + "identifier": { + "type": "array", + "description": "An identifier for the patient", + "items": { + "type": "object", + "properties": { + "system": { + "type": "string", + "description": "The namespace for the identifier value" + }, + "value": { + "type": "string", + "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." + } + } + } + }, + "name": { + "type": "array", + "description": "Patient name as registered, or as recorded by the user where the patient record cannot be traced. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", + "items": { + "type": "string" + } + } + }, + "required": ["family", "given"] + } + }, + "gender": { + "type": "string", + "description": "male | female | other | unknown" + }, + "birthDate": { + "type": "string", + "description": "The date of birth for the individual" + }, + "address": { + "type": "array", + "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", + "items": { + "type": "object", + "properties": { + "postalCode": { + "type": "string", + "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" + } + }, + "required": ["postalCode"] + } + } + }, + "required": [ + "resourceType", + "id", + "name", + "gender", + "birthDate", + "address" + ] + } + ] + } + }, + "extension": { + "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["url", "valueCodeableConcept"], + "properties": { + "url": { + "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "type": "string", + "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + }, + "valueCodeableConcept": { + "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "array", + "items": { + "type": "object", + "required": ["system", "code"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "A particular code in the system.", + "type": "string", + "example": "1303503001" + }, + "display": { + "description": "Representation defined by the system.", + "type": "string", + "example": "Administration of RSV (respiratory syncytial virus) vaccine" + } + } + } + }, + "text": { + "description": "Plain text representation of the concept.", + "type": "string" + } + } + } + } + } + }, + "identifier": { + "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["system", "value"], + "properties": { + "use": { + "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", + "type": "string", + "enum": [ + "usual", + "official", + "temp", + "secondary", + "old" + ], + "example": "official" + }, + "system": { + "description": "A URI for the system that has allocated the vaccination identifier.", + "type": "string", + "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" + }, + "value": { + "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", + "type": "string", + "example": "e2154d29-1ead-4830-a513-0d59705078fa" + } + } + } + }, + "status": { + "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", + "type": "string", + "enum": ["completed"], + "example": "completed" + }, + "vaccineCode": { + "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccine product details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccine product.", + "type": "string", + "example": "42605811000001109" + }, + "display": { + "description": "Description of the vaccine product.", + "type": "string", + "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + } + } + } + } + }, + "patient": { + "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", + "type": "object", + "required": ["reference"], + "properties": { + "reference": { + "description": "Reference of patient from contained section", + "type": "string", + "example": "#Pat1" + } + } + }, + "occurrence[X]": { + "type": "object", + "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", + "properties": { + "occurrenceDateTime": { + "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \n Only positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + } + }, + "required": ["occurrenceDateTime"] + }, + "recorded": { + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + }, + "primarySource": { + "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", + "type": "boolean", + "example": true + }, + "location": { + "type": "object", + "description": "The service delivery location where the vaccine administration occurred.", + "properties": { + "identifier": { + "type": "object", + "description": "An identifier for the service delivery location.", + "properties": { + "system": { + "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", + "type": "string", + "example": "X99999" + } + }, + "required": ["system", "value"] + } + } + }, + "manufacturer": { + "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", + "type": "object", + "properties": { + "display": { + "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "AstraZeneca Ltd" + } + } + }, + "lotNumber": { + "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", + "type": "string", + "example": "4120Z001" + }, + "expirationDate": { + "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "2021-04-29" + }, + "site": { + "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination body site details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination body site.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination body site.", + "type": "string", + "example": "368208006" + }, + "display": { + "description": "Description of the vaccination body site.", + "type": "string", + "example": "Left upper arm structure (body structure)" + } + } + } + } + } + }, + "route": { + "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination route details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination route.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination route.", + "type": "string", + "example": "78421000" + }, + "display": { + "description": "Description of the vaccination route.", + "type": "string", + "example": "Intramuscular route (qualifier value)" + } + } + } + } + } + }, + "doseQuantity": { + "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", + "type": "object", + "properties": { + "value": { + "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", + "type": "number", + "example": 1 + }, + "unit": { + "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "milliliter" + }, + "system": { + "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "http://unitsofmeasure.org" + }, + "code": { + "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "ml" + } + } + }, + "performer": { + "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["actor"], + "properties": { + "actor": { + "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", + "type": "object", + "properties": { + "type": { + "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", + "type": "string", + "example": "Organisation" + }, + "identifier": { + "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", + "type": "string", + "example": "B0C4P" + } + } + }, + "reference": { + "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", + "type": "string", + "example": "#Pract1" + } + } + } + } + } + }, + "reasonCode": { + "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", + "type": "array", + "items": { + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the reason code details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe the reason for administration of vaccine.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccination reason.", + "type": "string", + "example": "443684005" + }, + "display": { + "description": "Description of the vaccination reason.", + "type": "string", + "example": "Disease outbreak (event)" + } + } + } + } + } + } + }, + "protocolApplied": { + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["targetDisease", "doseNumber[X]"], + "properties": { + "targetDisease": { + "type": "array", + "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. See the provided [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", + "items": { + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "type": "array", + "description": "A reference to a code defined by a terminology system.", + "items": { + "type": "object", + "required": ["system", "code"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string" + }, + "code": { + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", + "type": "string" + }, + "display": { + "description": "A representation of the meaning of the code in the system, following the rules of the system.", + "type": "string" + } + } + } + } + } + } + }, + "doseNumber[X]": { + "type": "object", + "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", + "properties": { + "doseNumberPositiveInt": { + "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available.. Maximum value is 9.", + "type": "integer", + "maximum": 9, + "example": 1 + }, + "doseNumberString": { + "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", + "type": "string" + } + } + } + } + } + } + } + }, + "example": { + "resourceType": "Immunization", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449310475" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "a7437179-e86e-4855-b68e-24b5jhg3g" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07T13:28:17.271+00:00", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "X99999" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Create Immunization operation successful", + "headers": { + "Location": { + "$ref": "#/components/headers/Location" + }, + "CorrelationID": { + "$ref": "#/components/headers/CorrelationID" + }, + "RequestID": { + "$ref": "#/components/headers/RequestID" + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms-create" + } + } + }, + "get": { + "summary": "Search for a patient's immunisation records", + "operationId": "searchImmunization", + "description": "## Overview\nUse this interaction to search for a patient's vaccination records using their NHS number and DiseaseType. You can request the patient's vaccination history for one or more specified 'disease types'. You may limit the vaccination records by specifying date criteria, for example if you only need to know about vaccinations administered in the last 12 months. \n Location related data items are included. Patient location sensitivity indicators (such as flags for sensitive patient records) should be used to apply data filtering as appropriate. The response will not include contained resources for patient or practitioner within each immunization resource it returns. A single, separate patient resource will be included in the bundle and referenced by each immunization. \nVaccination events submitted without an NHS Number will not be available for retrieval via this interaction. Also, where a patient has a change of NHS Number some or all records may be unavailable via this interaction for a short period of time while records are updated. \nYou must be authorised for the search interaction and the disease type(s) specified in your search in order to access the records. \n \n### Search using POST\n \nA POST search interaction is supported in accordance with [FHIR guidance](https://digital.nhs.uk/developer/guides-and-documentation/our-api-technologies#fhir) as an alternative to a search with the GET verb. A POST search allows you to supply some or all parameters in the body of the request should you need to do so. It offers the same search functionality as the GET search interaction. \n\nNote that the API call for the POST search is different: \n\n`POST /Immunization/_search`\n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation history found | `patient.identifier`=https://fhir.nhs.uk/Id/nhs-number|9000000009 | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass required fields `patient.identifier` or `-immunization.target` | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/PatientIdentifier" + }, + { + "$ref": "#/components/parameters/ImmunizationTarget" + }, + { + "$ref": "#/components/parameters/DateFrom" + }, + { + "$ref": "#/components/parameters/DateTo" + }, + { + "$ref": "#/components/parameters/Include" + } + ], + "responses": { + "200": { + "description": "Search immunisation operation successful", + "content": { + "application/fhir+json": { + "schema": { + "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", + "type": "object", + "required": ["resourceType", "type", "total", "entry"], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Bundle`.", + "type": "string", + "example": "Bundle" + }, + "type": { + "description": "Indicates how the bundle is intended to be used. Always `searchset`.", + "type": "string", + "example": "searchset" + }, + "link": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relation": { + "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1). Always `Self`.", + "type": "string" + }, + "url": { + "description": "A url representing the search applied by the API to generate the result which may differ from the request if unrecognised or unsupported parameters have been ignored.", + "type": "string" + } + }, + "required": ["relation", "url"] + } + }, + "entry": { + "description": "List of matching immunisations and associated patient. If there were no matching immunisations, this is an empty list.", + "type": "array", + "items": { + "type": "object", + "required": ["fullUrl", "resource", "search"], + "properties": { + "fullUrl": { + "description": "URI for the Immunization or Patient resource.", + "type": "string", + "example": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c" + }, + "resource": { + "description": "The Immunization or Patient resource.", + "oneOf": [ + { + "description": "A matching immunisation, formatted as a FHIR Immunization resource.", + "type": "object", + "required": [ + "resourceType", + "extension", + "identifier", + "status", + "vaccineCode", + "patient", + "occurrence[X]", + "recorded", + "primarySource", + "location", + "performer", + "protocolApplied" + ], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Immunization`.", + "type": "string", + "example": "Immunization" + }, + "id": { + "description": "Immunization record Id.", + "type": "string", + "example": "191f288a-17f3-4cd5-a33c-a52aade6473c" + }, + "meta": { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed.", + "example": "2017-01-01T00:00:00Z" + }, + "source": { + "type": "string", + "description": "Identifies where the resource comes from." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + }, + "required": ["versionId"] + }, + "extension": { + "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": [ + "url", + "valueCodeableConcept" + ], + "properties": { + "url": { + "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "type": "string", + "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + }, + "valueCodeableConcept": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "array", + "items": { + "type": "object", + "required": [ + "system", + "code", + "display" + ], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "A particular code in the system.", + "type": "string", + "example": "1303503001" + }, + "display": { + "description": "Representation defined by the system.", + "type": "string", + "example": "Administration of RSV (respiratory syncytial virus) vaccine" + } + } + } + } + } + } + } + } + }, + "identifier": { + "description": "Unique identifier for this immunisation record, as generated by the source system.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["system", "value"], + "properties": { + "use": { + "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", + "type": "string", + "enum": [ + "usual", + "official", + "temp", + "secondary", + "old" + ], + "example": "official" + }, + "system": { + "description": "URI of the namespace of this identifier.", + "type": "string", + "example": "https://supplierABC/identifiers/vacc" + }, + "value": { + "description": "Identifier value within `system`.", + "type": "string", + "example": "e2154d29-1ead-4830-a513-0d59705078fa" + } + } + } + }, + "status": { + "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", + "type": "string", + "enum": ["completed"], + "example": "completed" + }, + "vaccineCode": { + "description": "Vaccine product administered.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccine product details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "system", + "code", + "display" + ], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccine product.", + "type": "string", + "example": "42605811000001109" + }, + "display": { + "description": "Description of the vaccine product.", + "type": "string", + "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + } + } + } + } + }, + "patient": { + "description": "The patient who was immunised.", + "type": "object", + "required": [ + "reference", + "type", + "identifier" + ], + "properties": { + "reference": { + "description": "URI for the associated Patient resource in the bundle.", + "type": "string", + "example": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" + }, + "type": { + "description": "Type of resource this reference refers to. Always `Patient`.", + "type": "string", + "example": "Patient" + }, + "identifier": { + "description": "Business identifier for linked Patient. Always an NHS number.", + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "URI of coding system used to identify linked patient. Always https://fhir.nhs.uk/Id/nhs-number", + "type": "string", + "example": "https://fhir.nhs.uk/Id/nhs-number" + }, + "value": { + "description": "Value in coding system representing linked patient.", + "type": "string", + "example": "9000000009" + } + } + } + } + }, + "occurrence[X]": { + "type": "object", + "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", + "properties": { + "occurrenceDateTime": { + "description": "Date and time of immunisation.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + } + }, + "required": ["occurrenceDateTime"] + }, + "recorded": { + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + }, + "primarySource": { + "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", + "type": "boolean", + "example": true + }, + "location": { + "type": "object", + "description": "The service delivery location where the vaccine administration occurred.", + "properties": { + "identifier": { + "type": "object", + "description": "An identifier for the service delivery location.", + "properties": { + "system": { + "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", + "type": "string", + "example": "X99999" + } + }, + "required": ["system", "value"] + } + }, + "required": ["identifier"] + }, + "manufacturer": { + "description": "Vaccine manufacturer details.", + "type": "object", + "properties": { + "display": { + "description": "Decsription of the vaccine manufacturer.", + "type": "string", + "example": "AstraZeneca Ltd" + } + } + }, + "lotNumber": { + "description": "Lot number of the vaccine product.", + "type": "string", + "example": "4120Z001" + }, + "expirationDate": { + "description": "Date vaccine batch expires.", + "type": "string", + "example": "2021-04-29" + }, + "site": { + "description": "Body site where vaccine was administered.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination body site details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination body site.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination body site.", + "type": "string", + "example": "368208006" + }, + "display": { + "description": "Description of the vaccination body site.", + "type": "string", + "example": "Left upper arm structure (body structure)" + } + } + } + } + }, + "required": ["coding"] + }, + "route": { + "description": "The path by which the vaccine product is taken into the body.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination route details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination route.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination route.", + "type": "string", + "example": "78421000" + }, + "display": { + "description": "Description of the vaccination route.", + "type": "string", + "example": "Intramuscular route (qualifier value)" + } + } + } + } + } + }, + "doseQuantity": { + "description": "The quantity of vaccine product that was administered.", + "type": "object", + "properties": { + "value": { + "description": "Number of units administered.", + "type": "number", + "example": 1 + }, + "unit": { + "description": "Description of unit.", + "type": "string", + "example": "milliliter" + }, + "system": { + "description": "System that defines coded unit form.", + "type": "string", + "example": "http://unitsofmeasure.org" + }, + "code": { + "description": "Code describing the unit.", + "type": "string", + "example": "ml" + } + } + }, + "performer": { + "description": "Details of the organisation that performed the immunisation.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["actor"], + "properties": { + "actor": { + "description": "Organisation that performed the immunisation.", + "type": "object", + "required": ["type", "identifier"], + "properties": { + "type": { + "description": "Type of actor. Always `Organisation`.", + "type": "string", + "example": "Organisation" + }, + "identifier": { + "description": "Organisation identifier.", + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "Organisation's ODS code.", + "type": "string", + "example": "B0C4P" + } + } + }, + "display": { + "description": "Organisation that performed the immunisation.", + "type": "string", + "example": "UNIVERSITY HOSPITAL OF WALES" + } + } + } + } + } + }, + "reasonCode": { + "description": "Reasons why the vaccine was administered.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the reason code details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "system", + "code", + "display" + ], + "properties": { + "system": { + "description": "Coding system used to describe the reason for administration of vaccine.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccination reason.", + "type": "string", + "example": "443684005" + }, + "display": { + "description": "Description of the vaccination reason.", + "type": "string", + "example": "Disease outbreak (event)" + } + } + } + } + } + } + }, + "protocolApplied": { + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "targetDisease", + "doseNumber[X]" + ], + "properties": { + "targetDisease": { + "type": "array", + "description": "The vaccine preventable disease the dose is being administered against.", + "items": { + "type": "object", + "properties": { + "coding": { + "type": "array", + "description": "A reference to a code defined by a terminology system.", + "items": { + "type": "object", + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string" + }, + "code": { + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", + "type": "string" + }, + "display": { + "description": "A representation of the meaning of the code in the system, following the rules of the system.", + "type": "string" + } + }, + "required": [ + "system", + "code", + "display" + ] + } + } + }, + "required": ["coding"] + } + }, + "doseNumber[X]": { + "type": "object", + "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", + "properties": { + "doseNumberPositiveInt": { + "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", + "type": "integer", + "maximum": 9, + "example": 1 + }, + "doseNumberString": { + "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", + "type": "string" + } + } + } + } + } + } + } + }, + { + "description": "Demographic information about the patient receiving an immunisation.", + "type": "object", + "required": ["resourceType", "id"], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Patient`.", + "type": "string", + "example": "Patient" + }, + "id": { + "description": "Patient ID (NHS Number)", + "type": "string", + "example": "9000000009" + }, + "identifier": { + "description": "Unique identifier for this patient. Always an NHS number.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "Coding system used to identify patients.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/nhs-number" + }, + "value": { + "description": "Code identifying the patient.", + "type": "string", + "example": "9000000009" + } + } + } + } + } + } + ] + }, + "search": { + "description": "Search-related information for the Immunization.", + "type": "object", + "required": ["mode"], + "properties": { + "mode": { + "description": "Indicates why this resource is in the result set. For Immunization resources this is always `match`.", + "enum": ["match", "include"] + } + } + } + } + } + }, + "total": { + "description": "Number of matching immunisations found.", + "type": "integer", + "example": 2 + } + } + }, + "example": { + "resourceType": "Bundle", + "type": "searchset", + "link": [ + { + "relation": "self", + "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=RSV&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9000000009" + } + ], + "entry": [ + { + "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", + "resource": { + "resourceType": "Immunization", + "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", + "meta": { + "versionId": "1" + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "identifier": [ + { + "use": "official", + "system": "https://supplierABC/identifiers/vacc", + "value": "e2154d29-1ead-4830-a513-0d59705078fa" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "X99999" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + }, + "display": "UNIVERSITY HOSPITAL OF WALES" + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005", + "display": "Disease outbreak (event)" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "resource": { + "resourceType": "Patient", + "id": "9000000009", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009" + } + ] + }, + "search": { + "mode": "include" + } + } + ], + "total": 1 + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms-search" + } + } + } + }, + "/Immunization/{id}": { + "get": { + "summary": "Retrieve a record of an immunisation by its unique identifier", + "operationId": "readImmunization", + "description": "## Overview\nThis interaction allows you to retrieve the record of a single vaccination by our assigned id. We will return the full immunization resource as submitted. \nThe response will include an eTag for the version of the record which has been returned. If you intend to update a record, it is recommended that you use this interaction to obtain the latest version (and eTag for the version). \nTo retrieve a full vaccination history for a patient, see the search interaction. \nYou must be authorised for the read interaction and the disease type associated with the vaccination event in order to access the record. \n\n## Sandbox testing\nYou can test the following scenarios in our sandbox environment:\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Immunisation record found | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 200 with immunisation data in response body |\n| Bad Request | Didn't pass required fields `id` | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "description": "Read Immunization operation successful", + "headers": { + "CorrelationID": { + "$ref": "#/components/headers/CorrelationID" + }, + "RequestID": { + "$ref": "#/components/headers/RequestID" + }, + "E-Tag": { + "$ref": "#/components/headers/E-Tag" + } + }, + "content": { + "application/fhir+json": { + "schema": { + "description": "A matching immunisation, formatted as a FHIR Immunization resource.", + "type": "object", + "required": [ + "resourceType", + "contained", + "extension", + "identifier", + "status", + "vaccineCode", + "patient", + "occurrence[X]", + "recorded", + "primarySource", + "location", + "performer", + "protocolApplied" + ], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Immunization`.", + "type": "string", + "example": "Immunization" + }, + "id": { + "description": "Immunization record Id.", + "type": "string", + "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" + }, + "meta": { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed.", + "example": "2017-01-01T00:00:00Z" + }, + "source": { + "type": "string", + "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + } + }, + "contained": { + "type": "array", + "description": "The schema for Practitioner & Patient are different. In response both Practitioner & Patient objects will be returned.", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Practitioner`.", + "example": "Practitioner" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact" + }, + "name": { + "type": "array", + "description": "The name(s) associated with the practitioner", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Given names (not always 'first').", + "items": { + "type": "string" + } + } + }, + "required": ["family", "given"] + } + } + }, + "required": ["resourceType", "id"] + }, + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Patient`.", + "example": "Patient" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact", + "example": "#Pat1" + }, + "identifier": { + "type": "array", + "description": "An identifier for the patient", + "items": { + "type": "object", + "properties": { + "system": { + "type": "string", + "description": "The namespace for the identifier value" + }, + "value": { + "type": "string", + "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." + } + } + } + }, + "name": { + "type": "array", + "description": "A name associated with the patient", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Given names (not always 'first').", + "items": { + "type": "string" + } + } + }, + "required": ["family", "given"] + } + }, + "gender": { + "type": "string", + "description": "male | female | other | unknown" + }, + "birthDate": { + "type": "string", + "description": "The date of birth for the individual" + }, + "address": { + "type": "array", + "description": "An address for the individual", + "items": { + "type": "object", + "properties": { + "postalCode": { + "type": "string", + "description": "Postal code for area" + } + }, + "required": ["postalCode"] + } + } + }, + "required": [ + "resourceType", + "id", + "name", + "gender", + "address" + ] + } + ] + } + }, + "extension": { + "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["url", "valueCodeableConcept"], + "properties": { + "url": { + "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "type": "string", + "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + }, + "valueCodeableConcept": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "array", + "items": { + "type": "object", + "required": ["system", "code", "display"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "A particular code in the system.", + "type": "string", + "example": "1303503001" + }, + "display": { + "description": "Representation defined by the system.", + "type": "string", + "example": "Administration of RSV (respiratory syncytial virus) vaccine" + } + } + } + } + } + } + } + } + }, + "identifier": { + "description": "Unique identifier for this immunisation record, as generated by the source system.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["system", "value"], + "properties": { + "use": { + "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", + "type": "string", + "enum": [ + "usual", + "official", + "temp", + "secondary", + "old" + ], + "example": "official" + }, + "system": { + "description": "URI of the namespace of this identifier.", + "type": "string", + "example": "https://supplierABC/identifiers/vacc" + }, + "value": { + "description": "Identifier value within `system`.", + "type": "string", + "example": "e2154d29-1ead-4830-a513-0d59705078fa" + } + } + } + }, + "status": { + "description": "Status of the immunisation event. This is *not* an indication of patient immunity, only whether the immunisation was completed or not. Currently we only return details of completed immunisations.", + "type": "string", + "enum": ["completed"], + "example": "completed" + }, + "vaccineCode": { + "description": "Vaccine product administered.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccine product details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["system", "code", "display"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccine product.", + "type": "string", + "example": "42605811000001109" + }, + "display": { + "description": "Description of the vaccine product.", + "type": "string", + "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + } + } + } + } + }, + "patient": { + "description": "The patient who was immunised.", + "type": "object", + "required": ["reference"], + "properties": { + "reference": { + "description": "Reference of patient from contained section", + "type": "string", + "example": "#Pat1" + } + } + }, + "occurrence[X]": { + "type": "object", + "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", + "properties": { + "occurrenceDateTime": { + "description": "Date and time of immunisation.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + } + }, + "required": ["occurrenceDateTime"] + }, + "recorded": { + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + }, + "primarySource": { + "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded.", + "type": "boolean", + "example": true + }, + "location": { + "type": "object", + "description": "The service delivery location where the vaccine administration occurred.", + "properties": { + "identifier": { + "type": "object", + "description": "An identifier for the service delivery location.", + "properties": { + "system": { + "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "A code from the system to represent the location. An ODS code of X99999 represents a location where a code is not available.", + "type": "string", + "example": "X99999" + } + }, + "required": ["system", "value"] + } + }, + "required": ["identifier"] + }, + "manufacturer": { + "description": "Vaccine manufacturer details.", + "type": "object", + "properties": { + "display": { + "description": "Decsription of the vaccine manufacturer.", + "type": "string", + "example": "AstraZeneca Ltd" + } + } + }, + "lotNumber": { + "description": "Lot number of the vaccine product.", + "type": "string", + "example": "4120Z001" + }, + "expirationDate": { + "description": "Date vaccine batch expires.", + "type": "string", + "example": "2021-04-29" + }, + "site": { + "description": "Body site where vaccine was administered.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination body site details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination body site.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination body site.", + "type": "string", + "example": "368208006" + }, + "display": { + "description": "Description of the vaccination body site.", + "type": "string", + "example": "Left upper arm structure (body structure)" + } + } + } + } + }, + "required": ["coding"] + }, + "route": { + "description": "The path by which the vaccine product is taken into the body.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination route details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination route.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination route.", + "type": "string", + "example": "78421000" + }, + "display": { + "description": "Description of the vaccination route.", + "type": "string", + "example": "Intramuscular route (qualifier value)" + } + } + } + } + } + }, + "doseQuantity": { + "description": "The quantity of vaccine product that was administered.", + "type": "object", + "properties": { + "value": { + "description": "Number of units administered.", + "type": "number", + "example": 1 + }, + "unit": { + "description": "Description of unit.", + "type": "string", + "example": "milliliter" + }, + "system": { + "description": "System that defines coded unit form.", + "type": "string", + "example": "http://unitsofmeasure.org" + }, + "code": { + "description": "Code describing the unit.", + "type": "string", + "example": "ml" + } + } + }, + "performer": { + "description": "Details of the organisation that performed the immunisation.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["actor"], + "properties": { + "actor": { + "description": "Organisation that performed the immunisation.", + "type": "object", + "required": ["type", "identifier"], + "properties": { + "type": { + "description": "Type of actor. Always `Organisation`.", + "type": "string", + "example": "Organisation" + }, + "identifier": { + "description": "Organisation identifier.", + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "Coding system used for the organisation identifier. Always `https://fhir.nhs.uk/Id/ods-organization-code`.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "Organisation's ODS code.", + "type": "string", + "example": "B0C4P" + } + } + }, + "display": { + "description": "Organisation that performed the immunisation.", + "type": "string", + "example": "UNIVERSITY HOSPITAL OF WALES" + } + } + } + } + } + }, + "reasonCode": { + "description": "Reasons why the vaccine was administered.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the reason code details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["system", "code", "display"], + "properties": { + "system": { + "description": "Coding system used to describe the reason for administration of vaccine.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccination reason.", + "type": "string", + "example": "443684005" + }, + "display": { + "description": "Description of the vaccination reason.", + "type": "string", + "example": "Disease outbreak (event)" + } + } + } + } + } + } + }, + "protocolApplied": { + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["targetDisease", "doseNumber[X]"], + "properties": { + "targetDisease": { + "type": "array", + "description": "The vaccine preventable disease the dose is being administered against.", + "items": { + "type": "object", + "properties": { + "coding": { + "type": "array", + "description": "A reference to a code defined by a terminology system.", + "items": { + "type": "object", + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string" + }, + "code": { + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", + "type": "string" + }, + "display": { + "description": "A representation of the meaning of the code in the system, following the rules of the system.", + "type": "string" + } + }, + "required": ["system", "code", "display"] + } + } + }, + "required": ["coding"] + } + }, + "doseNumber[X]": { + "type": "object", + "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", + "properties": { + "doseNumberPositiveInt": { + "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", + "type": "integer", + "maximum": 9, + "example": 1 + }, + "doseNumberString": { + "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", + "type": "string" + } + } + } + } + } + } + } + }, + "example": { + "resourceType": "Immunization", + "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Owl", + "given": ["Barney"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449310475" + } + ], + "name": [ + { + "family": "Owler", + "given": ["Ozzie"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "identifier": [ + { + "use": "official", + "system": "https://supplierABC/identifiers/vacc", + "value": "e2154d29-1ead-4830-a513-0d59705078fa" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "X99999" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms-read" + } + } + }, + "put": { + "summary": "Update a record of vaccination", + "operationId": "updateImmunization", + "description": "## Overview\nThis interaction allows you to add a new updated record of a vaccination event. Update replaces the full immunization resource, so you must provide all data fields, not just the change (Patch is not currently supported). You may obtain all the current data for the vaccination event using the read interaction, which will also return an eTag for the version. \nYou may use update to re-instate a deleted record, but our update interaction does not support creating a new vaccination event where one does not currently exist. \nYou must not change the identifier when updating a vaccination event. The identifier is used as a primary identifier by downstream systems. \nYou must be authorised for update interaction and the disease type associated with the vaccination event in order to update the record. \n\n## Sandbox testing \n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Update a vaccination event | Valid request as per schema | HTTP Status 200 |\n| Bad Request (missing/invalid required element in request body) | Didn't pass `E-Tag` in request header | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + }, + { + "$ref": "#/components/parameters/E-Tag" + } + ], + "requestBody": { + "content": { + "application/fhir+json": { + "schema": { + "description": "A FHIR Immunization resource.", + "type": "object", + "required": [ + "resourceType", + "id", + "contained", + "extension", + "identifier", + "status", + "vaccineCode", + "patient", + "occurrence[X]", + "recorded", + "primarySource", + "location", + "performer", + "protocolApplied" + ], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Immunization`.", + "type": "string", + "example": "Immunization" + }, + "id": { + "description": "Immunization record Id.", + "type": "string", + "example": "12a33650-6f94-4e8f-a971-1c5c41da5b22" + }, + "meta": { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed.", + "example": "2017-01-01T00:00:00Z" + }, + "source": { + "type": "string", + "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + } + }, + "contained": { + "type": "array", + "description": "Includes any relevant resources as defined within this specification and referenced from within the resource. A patient resource SHALL be included. \nThe schema for Practitioner & Patient are different.", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Practitioner`.", + "example": "Practitioner" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact" + }, + "name": { + "type": "array", + "description": "The name(s) associated with the practitioner", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Given names (not always 'first').", + "items": { + "type": "string" + } + } + } + } + } + }, + "required": ["resourceType", "id"] + }, + { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "description": "FHIR resource type. Always `Patient`.", + "example": "Patient" + }, + "id": { + "type": "string", + "description": "Logical id of this artifact", + "example": "#Pat1" + }, + "identifier": { + "type": "array", + "description": "An identifier for the patient", + "items": { + "type": "object", + "properties": { + "system": { + "type": "string", + "description": "The namespace for the identifier value" + }, + "value": { + "type": "string", + "description": "The value that is unique. \nThis SHALL be populated if `system` is https://fhir.nhs.uk/Id/nhs-number." + } + } + } + }, + "name": { + "type": "array", + "description": "Patient name as registered, or as recorded by the user where the patient record cannot be traced. \nThere SHOULD be only one instance of name. If more than one name instance is provided additional elements SHOULD be populated only so the current, official name can be determined or otherwise the current, official name SHALL be the first name instance. There SHALL be at least one name instance with both family and given elements populated.", + "items": { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "Family name (often called 'Surname')" + }, + "given": { + "type": "array", + "description": "Patient Forename. Middle names are not to be included within this field. \nThere SHOULD only be one given name supplied in this element.", + "items": { + "type": "string" + } + } + }, + "required": ["family", "given"] + } + }, + "gender": { + "type": "string", + "description": "male | female | other | unknown" + }, + "birthDate": { + "type": "string", + "description": "The date of birth for the individual" + }, + "address": { + "type": "array", + "description": "There SHOULD be only one instance of address with only the postalCode element populated. If more than one address instance is provided the additional elements SHOULD be populated only so the current, home post code can be determined or otherwise the current, home post code SHALL be the first address instance.", + "items": { + "type": "object", + "properties": { + "postalCode": { + "type": "string", + "description": "Patient residential/home postcode. Value should be divided into two parts separated by a single space, e.g. EC1A 1BB \nAs well as actual post codes, the following SHOULD be used in other scenarios. \n *ZZ99 3VZ No Fixed Abode \n *ZZ99 3WZ Address Not Known \n *ZZ99 3CZ (England/UK) Address not otherwise specified \nThe full list is available here: https://www.england.nhs.uk/wp-content/uploads/2020/04/cam-2021-guidance-v2.1.pdf" + } + }, + "required": ["postalCode"] + } + } + }, + "required": [ + "resourceType", + "id", + "name", + "gender", + "birthDate", + "address" + ] + } + ] + } + }, + "extension": { + "description": "FHIR extension wrapper for the vaccination procedure performed. Always contains exactly one object.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["url", "valueCodeableConcept"], + "properties": { + "url": { + "description": "URI for the type of extension - https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "type": "string", + "example": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure" + }, + "valueCodeableConcept": { + "description": "This SHALL be populated with the appropriate SNOMED CT code (identified by system=http://snomed.info/sct). \nThis relates to the vaccine that was administered, typically in the form of a procedure code. The UK Core IG provides guidance on codes for this extension, but the provider SHALL ensure the appropriate code and term is provided. \nAdditional coding MAY be included provided it is semantically equivalent to the SNOMED concept.", + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "description": "Wrapper for the vaccination procedure coding.", + "type": "array", + "items": { + "type": "object", + "required": ["system", "code"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "A particular code in the system.", + "type": "string", + "example": "1303503001" + }, + "display": { + "description": "Representation defined by the system.", + "type": "string", + "example": "Administration of RSV (respiratory syncytial virus) vaccine" + } + } + } + }, + "text": { + "description": "Plain text representation of the concept.", + "type": "string" + } + } + } + } + } + }, + "identifier": { + "description": "A unique identifier assigned to this immunization record. Only one identifier SHALL be provided.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["system", "value"], + "properties": { + "use": { + "description": "Identifier use as defined by https://www.hl7.org/fhir/valueset-identifier-use.html.", + "type": "string", + "enum": [ + "usual", + "official", + "temp", + "secondary", + "old" + ], + "example": "official" + }, + "system": { + "description": "A URI for the system that has allocated the vaccination identifier.", + "type": "string", + "example": "https://supplierABC/identifiers/vacc `or` https://supplierABC/ODSCode_NKO41/identifiers/vacc" + }, + "value": { + "description": "A unique identifier value within `system`. Ideally this would be a GUID / UUID. \nThe value in combination with the system SHALL be globally unique.", + "type": "string", + "example": "e2154d29-1ead-4830-a513-0d59705078fa" + } + } + } + }, + "status": { + "description": "Indicates the status of the immunization event. \nOnly administered vaccination records SHALL be supported: status = completed.", + "type": "string", + "enum": ["completed"], + "example": "completed" + }, + "vaccineCode": { + "description": "Vaccine product administered. \nWhere the vaccine product is known, the dm+d / SNOMED CT concept for the AMP form SHOULD be provided. \nWhere a meaningful vaccine code cannot be provided, use one of the following NullFlavor codes, \n NAVU - `Not available` \n UNC - `Unencoded` \n UNK - `Unknown` \n NA - `Not Applicable` \nFrom http://terminology.hl7.org/CodeSystem/v3-NullFlavor", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccine product details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccine product.", + "type": "string", + "example": "42605811000001109" + }, + "display": { + "description": "Description of the vaccine product.", + "type": "string", + "example": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + } + } + } + } + }, + "patient": { + "description": "The patient who received the immunization. \nWhen providing records of a vaccination event (create / update) and reading a record by its ID, this SHALL be a reference to a contained patient resource.", + "type": "object", + "required": ["reference"], + "properties": { + "reference": { + "description": "Reference of patient from contained section", + "type": "string", + "example": "#Pat1" + } + } + }, + "occurrence[X]": { + "type": "object", + "description": "When immunizations are given a specific date and time should always be known. The string data type for this element is not supported. Only occurrenceDateTime SHALL be used.", + "properties": { + "occurrenceDateTime": { + "description": "A dateTime format SHALL be provided. It SHOULD be to the level of precision as recorded within the source system, subject to the FHIR rules for dateTime. \n Only positive timezone offsets of '+00:00' (GMT) and '+01:00' (BST) are allowed. Where time zone information is required but is not available in the source system the time zone element can be a hardcoded static value of `+00:00`.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + } + }, + "required": ["occurrenceDateTime"] + }, + "recorded": { + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event.", + "type": "string", + "example": "2021-02-07T13:28:17.271000+00:00" + }, + "primarySource": { + "description": "Set as `TRUE` when the content of the record is based on information from the person performing the vaccine or who has clinical responsibility for the vaccination, and the system can be considered a primary source of the vaccination event. \nSet as `FALSE` when the content of the record is NOT based on information from the person performing the vaccine or who has clinical responsibility for the vaccination and the system should not be treated as a primary source for this record.", + "type": "boolean", + "example": true + }, + "location": { + "type": "object", + "description": "The service delivery location where the vaccine administration occurred.", + "properties": { + "identifier": { + "type": "object", + "description": "An identifier for the service delivery location.", + "properties": { + "system": { + "description": "The system which defines the location. Typically this will be https://fhir.nhs.uk/Id/ods-organization-code for a health setting (ODS use) or https://fhir.hl7.org.uk/Id/urn-school-number for an education setting (URN use). ", + "type": "string", + "example": "urn:iso:std:iso:3166" + }, + "value": { + "description": "The ODS or URN code of the location where the vaccination was administered. \n1. For occupational health vaccinations administered in a hospital trust by an independent healthcare provider, this SHALL be the ODS code of the hospital trust. \n2. For school vaccinations administered by a School Aged Immunisation Service provider, this SHALL be the URN of the school where the vaccination was administered. \n3. For roving teams on care home visits, this SHALL be the ODS code of the care home, where known. \n4. For any other vaccinations, populate with the same code as provided for `performer` ODS code. \n\nWhere the ODS/URN code is unavailable, a default value of `X99999` MUST be used.", + "type": "string", + "example": "GB" + } + }, + "required": ["system", "value"] + } + } + }, + "manufacturer": { + "description": "Manufacturer of vaccine product. This `SHOULD be populated` where the data is available.", + "type": "object", + "properties": { + "display": { + "description": "The free text name of the vaccine manufacturer. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "AstraZeneca Ltd" + } + } + }, + "lotNumber": { + "description": "Vaccine batch number. This should be captured at source ideally via use of automated scanning technology (GS1 GTIN / NTIN standard). \nThis `SHOULD be populated` where the data is available.", + "type": "string", + "example": "4120Z001" + }, + "expirationDate": { + "description": "Manufacturer expiry date or defrost expiry date of the vaccine, whichever is earliest. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "2021-04-29" + }, + "site": { + "description": "Body site where vaccine was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value from UK published reference set Vaccine body site of administration simple reference set (1127941000000100) should be used.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination body site details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination body site.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination body site.", + "type": "string", + "example": "368208006" + }, + "display": { + "description": "Description of the vaccination body site.", + "type": "string", + "example": "Left upper arm structure (body structure)" + } + } + } + } + } + }, + "route": { + "description": "The path by which the vaccine product is taken into the body. This `SHOULD be populated` where the data is available. \nA SNOMED-CT concept ID value from UK “ePrescribing route of administration simple reference set (foundation metadata concept)” (999000051000001100) should be used.", + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the vaccination route details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe vaccination route.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "Code for the vaccination route.", + "type": "string", + "example": "78421000" + }, + "display": { + "description": "Description of the vaccination route.", + "type": "string", + "example": "Intramuscular route (qualifier value)" + } + } + } + } + } + }, + "doseQuantity": { + "description": "The quantity of vaccine product that was administered. This `SHOULD be populated` where the data is available. \nA SNOMED-CT Concept ID value representing the unit of measure used SHOULD be provided.", + "type": "object", + "properties": { + "value": { + "description": "The actual value of the dose amount administered. This `SHOULD be populated` where the data is available. \nFor Example, \nComirnaty ® (Pfizer BioNTech): \n Full Dose (Primary Course or booster) = 0.3 \n Fractional Dose (Primary Course) = 0.1", + "type": "number", + "example": 1 + }, + "unit": { + "description": "A human-readable form of the unit. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "milliliter" + }, + "system": { + "description": "The code system from which the provided code is taken. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "http://unitsofmeasure.org" + }, + "code": { + "description": "The code for the unit of measure. SNOMED coded dose units are preferred. This `SHOULD be populated` where the data is available.", + "type": "string", + "example": "ml" + } + } + }, + "performer": { + "description": "Details of the organisation that performed the immunisation event. \nThis covers: \n The Commissioned Healthcare Provider who has administered the vaccination \n The professional performing the vaccination \nAt least one performer entry SHALL be provided which includes an actor with an identifier system and value.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["actor"], + "properties": { + "actor": { + "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated with `Organization`", + "type": "object", + "properties": { + "type": { + "description": "The type of actor reference provided. This SHALL be populated with `Organization`.", + "type": "string", + "example": "Organisation" + }, + "identifier": { + "description": "When the actor represents the managing organisation for the vaccination this SHALL be populated and the guidance for sub-elements applied.", + "type": "object", + "required": ["system", "value"], + "properties": { + "system": { + "description": "This SHALL be the system from which the supplied code is taken. The code SHOULD be an ODS code which comes from `https://fhir.nhs.uk/Id/ods-organization-code`.", + "type": "string", + "example": "https://fhir.nhs.uk/Id/ods-organization-code" + }, + "value": { + "description": "The ODS code for the Commissioned Healthcare Provider, \n For roving teams on home visits or care home visits, use the ODS code of the responsible site e.g. GP Practice or dedicated vaccination site \n For school vaccinations, use the ODS of code of the School Aged Immunisation Service provider, rather than the URN of the school \nURN codes must not be provided for this data item.", + "type": "string", + "example": "B0C4P" + } + } + }, + "reference": { + "description": "Where practitioner details are being provided, this SHOULD be a reference to a contained practitioner resource. If the actor is the managing organisation, this SHOULD be absent.", + "type": "string", + "example": "#Pract1" + } + } + } + } + } + }, + "reasonCode": { + "description": "A SNOMED-CT Concept representing the clinical indication or reason for administering or recording an historical vaccination. \nThe primary reason for the vaccination SHOULD be either the only reason submitted or the first SNOMED CT coded reason. \nThis `SHOULD be populated` where the data is available.", + "type": "array", + "items": { + "type": "object", + "properties": { + "coding": { + "description": "Wrapper for the reason code details.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "system": { + "description": "Coding system used to describe the reason for administration of vaccine.", + "type": "string", + "example": "http://snomed.info/sct" + }, + "code": { + "description": "SNOMED code for the vaccination reason.", + "type": "string", + "example": "443684005" + }, + "display": { + "description": "Description of the vaccination reason.", + "type": "string", + "example": "Disease outbreak (event)" + } + } + } + } + } + } + }, + "protocolApplied": { + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": ["targetDisease", "doseNumber[X]"], + "properties": { + "targetDisease": { + "type": "array", + "description": "The vaccine preventable disease the dose is being administered against. \nThis SHALL be populated with the appropriate SNOMED CT concept. See the provided [code list](https://digital.nhs.uk/developer/guides-and-documentation/building-healthcare-software/vaccinations/coding-for-vaccination-disease-types#how-this-applies-to-vaccinations-submitted-to-the-api) for each supported type of vaccination. A valid code or code combination SHALL be provided. \nFor vaccines which provide immunity for more than one target disease there SHALL be one instance of targetDisease for each and no more.", + "items": { + "type": "object", + "required": ["coding"], + "properties": { + "coding": { + "type": "array", + "description": "A reference to a code defined by a terminology system.", + "items": { + "type": "object", + "required": ["system", "code"], + "properties": { + "system": { + "description": "The identification of the code system that defines the meaning of the symbol in the code.", + "type": "string" + }, + "code": { + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system", + "type": "string" + }, + "display": { + "description": "A representation of the meaning of the code in the system, following the rules of the system.", + "type": "string" + } + } + } + } + } + } + }, + "doseNumber[X]": { + "type": "object", + "description": "Nominal position in a series. This SHALL be provided but may be populated using either of the dataTypes available: PositiveInt or String. The use of an integer is preferred. Maximum value is 9.", + "properties": { + "doseNumberPositiveInt": { + "description": "Nominal position in a course of vaccines. This `SHOULD be populated` where the data is available. Maximum value is 9.", + "type": "integer", + "maximum": 9, + "example": 1 + }, + "doseNumberString": { + "description": "Description of the dose sequence where it is not a numeric or a reason a dose number cannot be provided. \nA string should only be used in cases where an integer is not available.", + "type": "string" + } + } + } + } + } + } + } + }, + "example": { + "resourceType": "Immunization", + "id": "4ff607e0-c6e9-4fe0-a2b6-3bcd7fdc44c9", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Nightingale", + "given": ["Florence"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449310475" + } + ], + "name": [ + { + "family": "Taylor", + "given": ["Sarah"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "EC1A 1BB" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1324681000000101", + "display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)" + } + ] + } + } + ], + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "a7437179-e86e-4855-b68e-24b5jhg3g" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "39114911000001105", + "display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271+00:00", + "recorded": "2021-02-07T13:28:17.271+00:00", + "primarySource": true, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "location": { + "identifier": { + "value": "X99999", + "system": "https://fhir.nhs.uk/Id/ods-organization-code" + } + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "code": "443684005", + "system": "http://snomed.info/sct" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Update Immunization operation successful", + "headers": { + "CorrelationID": { + "$ref": "#/components/headers/CorrelationID" + }, + "RequestID": { + "$ref": "#/components/headers/RequestID" + } + } + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms-update" + } + } + }, + "delete": { + "summary": "Mark a record of vaccination as being entered in error", + "operationId": "deleteImmunization", + "description": "## Overview\nThis interaction allows you to mark a record that has been entered in error.\nDeleted records will continue to be stored for a period of time but are not returned in response to read or search requests.\nA deleted record can be re-instated using the update interaction if it was incorrectly deleted. \n\n## Sandbox testing\n\n| Scenario | Request | Response |\n| ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------|\n| | | |\n| Delete a vaccination event | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 204 No Content |\n| Bad Request | Didn't pass required fields `id` | HTTP Status 400 Bad Request |\n", + "parameters": [ + { + "$ref": "#/components/parameters/CorrelationID" + }, + { + "$ref": "#/components/parameters/RequestID" + }, + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "204": { + "description": "Delete Immunization operation successful" + }, + "4XX": { + "$ref": "#/components/responses/4XX-imms-delete" + } + } + } + } + }, + "components": { + "responses": { + "4XX-imms-create": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Invalid resourceType in body | {\"resourceType\": \"OperationOutcome\", \"id\": \"a7dc58e7-4033-43e6-b34e-cf7ee7bbc567\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: This service only accepts FHIR Immunization Resources (i.e. resourceType must equal 'Immunization')\"}]} |\n| 400 | Bad Request | Invalid resourceType within contained | {\"resourceType\": \"OperationOutcome\", \"id\": \"ffda54b5-21a7-4e4c-9ca5-0a4e510d7467\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: contained must contain only Patient and Practitioner resources\"}]} |\n| 400 | Bad Request | Invalid status value | {\"resourceType\": \"OperationOutcome\", \"id\": \"12a9f94c-df00-4e87-aefa-2c76f9065367\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: status must be one of the following: completed\"}]} |\n| 400 | Bad Request | Invalid value for a datetime (string) field e.g. occurrenceDateTime.
Note : The error format will remain same for any datetime (string) field; only the field name will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"e02e3254-c1b3-4afd-a8ca-84091d7d272c\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: occurrenceDateTime must be a valid datetime in the format 'YYYY-MM-DDThh:mm:ss+zz:zz' (where time element is optional, timezone must be given if and only if time is given, and milliseconds can be optionally included after the seconds). Note that partial dates are not allowed for occurrenceDateTime for this service.\"}]} |\n| 400 | Bad Request | Invalid value for a string field e.g. postalCode
Note : The error format will remain same for any string field; only the field location will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"0df23485-4690-41ab-8b24-8b28584ec2eb\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: contained[?(@.resourceType=='Patient')].address[0].postalCode must be a non-empty string\"}]} |\n| 400 | Bad Request | Invalid value for an integer field e.g. doseNumberPositiveInt
Note : The error format will remain same for any integer field; only the field location will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"dda6b6cd-af06-47ea-a4d4-f0f8cdf83085\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: protocolApplied[0].doseNumberPositiveInt must be a positive integer\"}]} |\n| 400 | Bad Request | Invalid top level element e.g. test | {\"resourceType\": \"OperationOutcome\", \"id\": \"fd60f75c-d2cb-4c77-b3e9-ba8c3e0379e6\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: \"test\" is not an allowed element of the Immunization resource for this service\"}]} |\n| 400 | Bad Request | Missing mandatory field e.g. contained
Note : The error format will remain same for any mandatory field; only the field name will change under diagnostics. | {\"resourceType\": \"OperationOutcome\", \"id\": \"72eee803-c8bf-4728-a15b-5d9a10bb645c\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"contained is a mandatory field\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 422 | Unprocessable Entity | Duplicate Identifier value | {\"resourceType\": \"OperationOutcome\", \"id\": \"b82adf54-1844-4354-a5dd-09bfc34dd569\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"duplicate\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"DUPLICATE\"}]}, \"diagnostics\": \"The provided identifier: https://supplierABC/identifiers/vacc#a7437179-e86e-4855-b68e-xxxxx is duplicated\"}]} |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] + } + } + } + }, + "4XX-imms-search": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Search parameter immunization.target is either missing or not in the expected format. | {\"resourceType\":\"OperationOutcome\",\"id\":\"0f985bbd-a25e-4644-bcab-e11ac9f1cf3a\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"Search parameter -immunization.target must have one or more values.\"}]} |\n| 400 | Bad Request | Search parameter patient.identifier is either missing or not in the expected format. | {\"resourceType\":\"OperationOutcome\",\"id\":\"ee3ce747-95dc-40e9-be2e-404edf203444\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"Search parameter patient.identifier must have one value.\"}]} |\n| 400 | Bad Request | Invalid value for patient.identifier | {\"resourceType\":\"OperationOutcome\",\"id\":\"ec7f1d44-6658-42cc-bffb-af790beba382\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"invalid\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"INVALID\"}]},\"diagnostics\":\"patient.identifier must be in the format of \\\"https://fhir.nhs.uk/Id/nhs-number|{NHS number}\\\" e.g. \\\"https://fhir.nhs.uk/Id/nhs-number|9000000009\\\"\"}]} |\n| 400 | Bad Request | Invalid date.to/date.from format | {\"resourceType\": \"OperationOutcome\", \"id\": \"ad3d7881-6f2e-418f-ae13-75a07d7269d7\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"Search parameter -date.to must be in format: YYYY-MM-DD\"}]} |\n| 400 | Bad Request | Invalid value for \"-immunization.target\" | {\"resourceType\": \"OperationOutcome\", \"id\": \"f1753822-5667-4562-a288-1544a0b66d00\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"immunization-target must be one or more of the following: COVID19,FLU,HPV,MMR,3IN1,MENACWY,RSV\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] + } + } + } + }, + "4XX-imms-read": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Missing immunization event identifier (id) | {\"resourceType\": \"OperationOutcome\", \"id\": \"438f5c0d-f89d-46ec-b4ba-c08dc5a44ff3\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Unrecognized immunization event identifier (id) | {\"resourceType\": \"OperationOutcome\", \"id\": \"5b51e331-5f9c-442f-a4c3-b5f52a1ba83e\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"The requested resource was not found.\"}]} |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] + } + } + } + }, + "4XX-imms-update": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | All validation errors & mandatory field errors from the Record scenario | All validation errors & mandatory field errors from POST /Immunization |\n| 400 | Bad Request | Missing id in request parameter | {\"resourceType\": \"OperationOutcome\", \"id\": \"05ef00c0-18ae-4a88-93ca-9dc1b0532ea1\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 400 | Bad Request | Missing id parameter in request body
or
mismatch between id provided within request body and request parameter | {\"resourceType\": \"OperationOutcome\", \"id\": \"ddf659ac-1a34-4ca2-b11a-77b9a8febeec\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The provided immunization id:16ec10f2-afef-4c0b-9467-xxxx doesn't match with the content of the request body\"}]} |\n| 400 | Bad Request | Mismatch between identifier value and stored event | {\"resourceType\": \"OperationOutcome\", \"id\": \"ac43b4a6-7ceb-4ff8-a246-d7ceacdab058\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: identifier[0].value doesn't match with the stored content\"}]} |\n| 400 | Bad Request | Missing E-Tag (version) header | {\"resourceType\": \"OperationOutcome\", \"id\": \"715d5e23-4621-4719-8902-e80faf6539fd\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: Immunization resource version not specified in the request headers\"}]} |\n| 400 | Bad Request | Wrong version number passed in E-Tag header
e.g. passing version no. > 1 in case of first-time update | {\"resourceType\": \"OperationOutcome\", \"id\": \"7cf8b2e1-f069-4cce-9b48-5201f64a50a9\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource 5a64fa79-9114-49bb-97c9-d455b0276475 version is inconsistent with the existing version.\"}]} |\n| 400 | Bad Request | Empty value for E-Tag header | {\"resourceType\": \"OperationOutcome\", \"id\": \"6eda36fc-777a-472a-b3a3-3e1627eca980\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: Immunization resource version: in the request headers is invalid.\"}]} |\n| 400 | Bad Request | E-Tag value <= current stored value | {\"resourceType\": \"OperationOutcome\", \"id\": \"06e3086a-61b5-4dfc-b0ff-10e2658268f5\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invariant\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVARIANT\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource 5a64fa79-9114-49bb-97c9-d455b0276475 has changed since the last retrieve.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Provided id not available | {\"resourceType\": \"OperationOutcome\", \"id\": \"c86be3de-bcbb-4ba5-902e-9acf55187dc5\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"Validation errors: The requested immunization resource with id:16ec10f2-afef-4c0b-9467-80434a7a0e26 was not found.\"}]} |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] + } + } + } + }, + "4XX-imms-delete": { + "description": "Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault.\n\n| HTTP status | Error code | Description | Example |\n| ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------|\n| 400 | Bad Request | Missing or invalid id | {\"resourceType\": \"OperationOutcome\", \"id\": \"f400ad2c-62d9-4f98-bc35-a146caf14dee\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"invalid\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"INVALID\"}]}, \"diagnostics\": \"the provided event ID is either missing or not in the expected format.\"}]} |\n| 401 | Unauthorized | Authorization is required for the interaction that was attempted | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"expired\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender has not provided a token or it has expired or is otherwise invalid.\"}]} |\n| 403 | Forbidden | The sender does not have permissions to access this resource | {\"resourceType\":\"OperationOutcome\",\"id\":\"a5abca2a-4eda-41da-b2cc-95d48c6b791d\",\"meta\":{\"profile\":[\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]},\"issue\":[{\"severity\":\"error\",\"code\":\"forbidden\",\"details\":{\"coding\":[{\"system\":\"https://fhir.nhs.uk/Codesystem/http-error-codes\",\"code\":\"SEND_UNAUTHORIZED\"}]},\"diagnostics\":\"The sender does not have permissions to access this resource. Please check your credentials and permissions.\"}]} |\n| 403 | Forbidden | The sender does not have permission for the specific operation or vaccine type | {\"resourceType\": \"OperationOutcome\", \"id\": \"1b7eec0a-316f-4f6e-a342-4b37f6705050\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"forbidden\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"FORBIDDEN\"}]}, \"diagnostics\": \"Unauthorized request for vaccine type\"}]} |\n| 404 | Not Found | Non existing id in query parameter
or
Trying to deleted an already deleted record | {\"resourceType\": \"OperationOutcome\", \"id\": \"f772ee3e-3baf-4934-a07a-a13f70b1a50e\", \"meta\": {\"profile\": [\"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome\"]}, \"issue\": [{\"severity\": \"error\", \"code\": \"not-found\", \"details\": {\"coding\": [{\"system\": \"https://fhir.nhs.uk/Codesystem/http-error-codes\", \"code\": \"NOT-FOUND\"}]}, \"diagnostics\": \"Immunization resource does not exist. ID: 5a64fa79-9114-49bb-97c9-xxxxxxxx\"}]} |\n", + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/OperationOutcome" + }, + "example": { + "resourceType": "OperationOutcome", + "id": "a5abca2a-4eda-41da-b2cc-95d48c6b791d", + "meta": { + "profile": [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" + ] + }, + "issue": [ + { + "severity": "error", + "code": "expired", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "code": "SEND_UNAUTHORIZED" + } + ] + }, + "diagnostics": "The sender has not provided a token or it has expired or is otherwise invalid." + } + ] + } + } + } + } + }, + "requestBodies": { + "Immunization": { + "content": { + "application/fhir+json": { + "schema": { + "$ref": "#/components/schemas/Immunization" + } + } + }, + "required": true + }, + "SearchImmunization": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "patient.identifier": { + "type": "string", + "description": "The patient's NHS number.\nExpressed as `` where`` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", + "example": "9000000009" + }, + "-immunization.target": { + "type": "string", + "description": "Specific procedures, disorders, diseases, infections or organisms.\n", + "enum": [ + "COVID19", + "FLU", + "MMR", + "HPV", + "3IN1", + "MENACWY", + "RSV" + ] + }, + "-date.from": { + "type": "string", + "format": "date", + "description": "The earliest date to be included (e.g. 2020-01-01)", + "default": "1900-01-01" + }, + "-date.to": { + "type": "string", + "format": "date", + "description": "The latest date to be included (e.g. 2020-12-31)", + "default": "9999-12-31" + }, + "_include": { + "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", + "type": "string", + "default": "Immunization:patient" + } + } + } + } + } + } + }, + "headers": { + "CorrelationID": { + "required": false, + "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "RequestID": { + "required": false, + "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "Location": { + "required": true, + "description": "The location of the newly created Immunization record. It contains the resource ID at the end.", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "E-Tag": { + "required": true, + "description": "Indicates the current version of an Immunization resource", + "schema": { + "type": "integer", + "example": 1 + } + } + }, + "parameters": { + "Id": { + "in": "path", + "name": "id", + "required": true, + "description": "A required ID which you can use to identify an Immunization event object.\n\nMirrored back in a response header.\n", + "schema": { + "type": "string", + "example": "29dc4e84-7e72-11ee-b962-0242ac120002" + } + }, + "CorrelationID": { + "in": "header", + "name": "X-Correlation-ID", + "required": false, + "description": "An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters.\n\nMirrored back in a response header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "RequestID": { + "in": "header", + "name": "X-Request-ID", + "required": false, + "description": "A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk.\n\nMust be a universally unique identifier (UUID) (ideally version 4).\n\nMirrored back in a response header.\n\nIf you re-send a failed request, use the same value in this header.\n", + "schema": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "example": "60E0B220-8136-4CA5-AE46-1D97EF59D068" + } + }, + "PatientIdentifier": { + "in": "query", + "name": "patient.identifier", + "description": "The patient's NHS number.\nExpressed as `|` where `` must be `https://fhir.nhs.uk/Id/nhs-number` and `` must be a [valid NHS number](https://www.datadictionary.nhs.uk/attributes/nhs_number.html).\n", + "required": true, + "schema": { + "type": "string", + "example": "https://fhir.nhs.uk/Id/nhs-number|9000000009" + } + }, + "ImmunizationTarget": { + "in": "query", + "name": "-immunization.target", + "description": "Specific procedures, disorders, diseases, infections or organisms.\n", + "required": true, + "schema": { + "type": "string", + "enum": ["COVID19", "FLU", "MMR", "HPV", "3IN1", "MENACWY", "RSV"] + } + }, + "DateFrom": { + "in": "query", + "name": "-date.from", + "description": "The earliest date to be included (e.g. 2020-01-01)", + "schema": { + "type": "string", + "format": "date", + "default": "1900-01-01" + } + }, + "DateTo": { + "in": "query", + "name": "-date.to", + "description": "The latest date to be included (e.g. 2020-12-31)", + "schema": { + "type": "string", + "format": "date", + "default": "9999-12-31" + } + }, + "Include": { + "in": "query", + "name": "_include", + "description": "Specifies other resources to be included in the response along with the immunisations.\nMust be `Immunization:patient`, which will include patient demographic details.", + "required": false, + "schema": { + "type": "string", + "default": "Immunization:patient" + } + }, + "E-Tag": { + "in": "header", + "name": "E-Tag", + "required": true, + "description": "Indicates the current version of an Immunization resource", + "schema": { + "type": "integer", + "example": 1 + } + } + }, + "schemas": { + "Resource": { + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "enum": [ + "Resource", + "DomainResource", + "Account", + "ActivityDefinition", + "AdverseEvent", + "AllergyIntolerance", + "Appointment", + "AppointmentResponse", + "AuditEvent", + "Basic", + "Binary", + "BiologicallyDerivedProduct", + "BodyStructure", + "Bundle", + "CapabilityStatement", + "CarePlan", + "CareTeam", + "CatalogEntry", + "ChargeItem", + "ChargeItemDefinition", + "Claim", + "ClaimResponse", + "ClinicalImpression", + "CodeSystem", + "Communication", + "CommunicationRequest", + "CompartmentDefinition", + "Composition", + "ConceptMap", + "Condition", + "Consent", + "Contract", + "Coverage", + "CoverageEligibilityRequest", + "CoverageEligibilityResponse", + "DetectedIssue", + "Device", + "DeviceDefinition", + "DeviceMetric", + "DeviceRequest", + "DeviceUseStatement", + "DiagnosticReport", + "DocumentManifest", + "DocumentReference", + "EffectEvidenceSynthesis", + "Encounter", + "Endpoint", + "EnrollmentRequest", + "EnrollmentResponse", + "EpisodeOfCare", + "EventDefinition", + "Evidence", + "EvidenceVariable", + "ExampleScenario", + "ExplanationOfBenefit", + "FamilyMemberHistory", + "Flag", + "Goal", + "GraphDefinition", + "Group", + "GuidanceResponse", + "HealthcareService", + "ImagingStudy", + "Immunization", + "ImmunizationEvaluation", + "ImmunizationRecommendation", + "ImplementationGuide", + "InsurancePlan", + "Invoice", + "Library", + "Linkage", + "List", + "Location", + "Measure", + "MeasureReport", + "Media", + "Medication", + "MedicationAdministration", + "MedicationDispense", + "MedicationKnowledge", + "MedicationRequest", + "MedicationStatement", + "MedicinalProduct", + "MedicinalProductAuthorization", + "MedicinalProductContraindication", + "MedicinalProductIndication", + "MedicinalProductIngredient", + "MedicinalProductInteraction", + "MedicinalProductManufactured", + "MedicinalProductPackaged", + "MedicinalProductPharmaceutical", + "MedicinalProductUndesirableEffect", + "MessageDefinition", + "MessageHeader", + "MolecularSequence", + "NamingSystem", + "NutritionOrder", + "Observation", + "ObservationDefinition", + "OperationDefinition", + "OperationOutcome", + "Organization", + "OrganizationAffiliation", + "Parameters", + "Patient", + "PaymentNotice", + "PaymentReconciliation", + "Person", + "PlanDefinition", + "Practitioner", + "PractitionerRole", + "Procedure", + "Provenance", + "Questionnaire", + "QuestionnaireResponse", + "RelatedPerson", + "RequestGroup", + "ResearchDefinition", + "ResearchElementDefinition", + "ResearchStudy", + "ResearchSubject", + "RiskAssessment", + "RiskEvidenceSynthesis", + "Schedule", + "SearchParameter", + "ServiceRequest", + "Slot", + "Specimen", + "SpecimenDefinition", + "StructureDefinition", + "StructureMap", + "Subscription", + "Substance", + "SubstanceNucleicAcid", + "SubstancePolymer", + "SubstanceProtein", + "SubstanceReferenceInformation", + "SubstanceSourceMaterial", + "SubstanceSpecification", + "SupplyDelivery", + "SupplyRequest", + "Task", + "TerminologyCapabilities", + "TestReport", + "TestScript", + "ValueSet", + "VerificationResult", + "VisionPrescription" + ] + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + } + }, + "required": ["resourceType"] + }, + "DomainResource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/Narrative", + "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." + }, + "contained": { + "type": "array", + "items": { + "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "enum": [ + "Resource", + "DomainResource", + "Account", + "ActivityDefinition", + "AdverseEvent", + "AllergyIntolerance", + "Appointment", + "AppointmentResponse", + "AuditEvent", + "Basic", + "Binary", + "BiologicallyDerivedProduct", + "BodyStructure", + "Bundle", + "CapabilityStatement", + "CarePlan", + "CareTeam", + "CatalogEntry", + "ChargeItem", + "ChargeItemDefinition", + "Claim", + "ClaimResponse", + "ClinicalImpression", + "CodeSystem", + "Communication", + "CommunicationRequest", + "CompartmentDefinition", + "Composition", + "ConceptMap", + "Condition", + "Consent", + "Contract", + "Coverage", + "CoverageEligibilityRequest", + "CoverageEligibilityResponse", + "DetectedIssue", + "Device", + "DeviceDefinition", + "DeviceMetric", + "DeviceRequest", + "DeviceUseStatement", + "DiagnosticReport", + "DocumentManifest", + "DocumentReference", + "EffectEvidenceSynthesis", + "Encounter", + "Endpoint", + "EnrollmentRequest", + "EnrollmentResponse", + "EpisodeOfCare", + "EventDefinition", + "Evidence", + "EvidenceVariable", + "ExampleScenario", + "ExplanationOfBenefit", + "FamilyMemberHistory", + "Flag", + "Goal", + "GraphDefinition", + "Group", + "GuidanceResponse", + "HealthcareService", + "ImagingStudy", + "Immunization", + "ImmunizationEvaluation", + "ImmunizationRecommendation", + "ImplementationGuide", + "InsurancePlan", + "Invoice", + "Library", + "Linkage", + "List", + "Location", + "Measure", + "MeasureReport", + "Media", + "Medication", + "MedicationAdministration", + "MedicationDispense", + "MedicationKnowledge", + "MedicationRequest", + "MedicationStatement", + "MedicinalProduct", + "MedicinalProductAuthorization", + "MedicinalProductContraindication", + "MedicinalProductIndication", + "MedicinalProductIngredient", + "MedicinalProductInteraction", + "MedicinalProductManufactured", + "MedicinalProductPackaged", + "MedicinalProductPharmaceutical", + "MedicinalProductUndesirableEffect", + "MessageDefinition", + "MessageHeader", + "MolecularSequence", + "NamingSystem", + "NutritionOrder", + "Observation", + "ObservationDefinition", + "OperationDefinition", + "OperationOutcome", + "Organization", + "OrganizationAffiliation", + "Parameters", + "Patient", + "PaymentNotice", + "PaymentReconciliation", + "Person", + "PlanDefinition", + "Practitioner", + "PractitionerRole", + "Procedure", + "Provenance", + "Questionnaire", + "QuestionnaireResponse", + "RelatedPerson", + "RequestGroup", + "ResearchDefinition", + "ResearchElementDefinition", + "ResearchStudy", + "ResearchSubject", + "RiskAssessment", + "RiskEvidenceSynthesis", + "Schedule", + "SearchParameter", + "ServiceRequest", + "Slot", + "Specimen", + "SpecimenDefinition", + "StructureDefinition", + "StructureMap", + "Subscription", + "Substance", + "SubstanceNucleicAcid", + "SubstancePolymer", + "SubstanceProtein", + "SubstanceReferenceInformation", + "SubstanceSourceMaterial", + "SubstanceSpecification", + "SupplyDelivery", + "SupplyRequest", + "Task", + "TerminologyCapabilities", + "TestReport", + "TestScript", + "ValueSet", + "VerificationResult", + "VisionPrescription" + ] + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + } + }, + "required": ["resourceType"] + } + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + } + } + }, + "Immunization": { + "type": "object", + "required": ["status", "vaccineCode", "patient"], + "properties": { + "text": { + "$ref": "#/components/schemas/Narrative", + "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." + }, + "contained": { + "type": "array", + "items": { + "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "example": "Immunization" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + } + }, + "required": ["resourceType"] + } + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + }, + "identifier": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Identifier", + "description": "A unique identifier assigned to this immunization record." + } + }, + "status": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Indicates the current status of the immunization event." + }, + "statusReason": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates the reason the immunization event was not performed." + }, + "vaccineCode": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Vaccine that was administered or was to be administered." + }, + "patient": { + "$ref": "#/components/schemas/Reference", + "description": "The patient who either received or did not receive the immunization." + }, + "encounter": { + "$ref": "#/components/schemas/Reference", + "description": "The visit or admission or other contact between patient and health care provider the immunization was performed as part of." + }, + "occurrenceDateTime": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date vaccine administered or was to be administered." + }, + "occurrenceString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Date vaccine administered or was to be administered." + }, + "recorded": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The date the occurrence of the immunization was first captured in the record - potentially significantly after the occurrence of the event." + }, + "primarySource": { + "type": "boolean", + "description": "An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded." + }, + "reportOrigin": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The source of the data when the report of the immunization event is not based on information from the person who administered the vaccine." + }, + "location": { + "$ref": "#/components/schemas/Reference", + "description": "The service delivery location where the vaccine administration occurred." + }, + "manufacturer": { + "$ref": "#/components/schemas/Reference", + "description": "Name of vaccine manufacturer." + }, + "lotNumber": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Lot number of the vaccine product." + }, + "expirationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", + "description": "Date vaccine batch expires." + }, + "site": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Body site where vaccine was administered." + }, + "route": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The path by which the vaccine product is taken into the body." + }, + "doseQuantity": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The quantity of vaccine product that was administered." + }, + "performer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Performer", + "description": "Indicates who performed the immunization event." + } + }, + "note": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Annotation", + "description": "Extra information about the immunization that is not conveyed by the other attributes." + } + }, + "reasonCode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Reasons why the vaccine was administered." + } + }, + "reasonReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference", + "description": "Condition, Observation or DiagnosticReport that supports why the immunization was administered." + } + }, + "isSubpotent": { + "type": "boolean", + "description": "Indication if a dose is considered to be subpotent. By default, a dose should be considered to be potent." + }, + "subpotentReason": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Reason why a dose is considered to be subpotent." + } + }, + "education": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Education", + "description": "Educational material presented to the patient (or guardian) at the time of vaccine administration." + } + }, + "programEligibility": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates a patient's eligibility for a funding program." + } + }, + "fundingSource": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Indicates the source of the vaccine actually administered. This may be different than the patient eligibility (e.g. the patient may be eligible for a publically purchased vaccine but due to inventory issues, vaccine purchased with private funds was actually administered)." + }, + "reaction": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_Reaction", + "description": "Categorical data indicating that an adverse event is associated in time to an immunization." + } + }, + "protocolApplied": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Immunization_ProtocolApplied", + "description": "The protocol (set of recommendations) being followed by the provider who administered the dose." + } + } + }, + "example": { + "resourceType": "Immunization", + "id": "12a33650-6f94-4e8f-a971-1c5c41da5b22", + "contained": [ + { + "resourceType": "Practitioner", + "id": "Pract1", + "name": [ + { + "family": "Owl", + "given": ["Barney"] + } + ] + }, + { + "resourceType": "Patient", + "id": "Pat1", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449310475" + } + ], + "name": [ + { + "family": "Owler", + "given": ["Ozzie"] + } + ], + "gender": "unknown", + "birthDate": "1965-02-28", + "address": [ + { + "postalCode": "ZZ99 3CZ" + } + ] + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "#Pat1" + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "urn:iso:std:iso:3166", + "value": "GB" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "reference": "#Pract1" + } + }, + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "N2N9I" + } + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + } + }, + "Bundle": { + "description": "FHIR Bundle containing the query results - a list of matching immunisations and associated patients.", + "type": "object", + "required": ["resourceType", "type"], + "properties": { + "resourceType": { + "description": "FHIR resource type. Always `Bundle`.", + "type": "string", + "example": "Bundle" + }, + "type": { + "description": "Indicates how the bundle is intended to be used. Always `searchset`.", + "type": "string", + "example": "searchset" + }, + "link": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Link", + "description": "A series of links that provide context to this bundle." + } + }, + "entry": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Entry", + "description": "An entry in a bundle resource - will either contain a resource or information about a resource (transactions and history only)." + } + }, + "total": { + "type": "integer", + "format": "int32", + "description": "If a set of search matches, this is the total number of entries of type 'match' across all pages in the search. It does not include search.mode = 'include' or 'outcome' entries and it does not provide a count of the number of entries in the Bundle." + } + }, + "example": { + "resourceType": "Bundle", + "type": "searchset", + "link": [ + { + "relation": "self", + "url": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization?immunization.target=COVID19&_include=Immunization%3Apatient&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9449306206" + } + ], + "entry": [ + { + "fullUrl": "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/191f288a-17f3-4cd5-a33c-a52aade6473c", + "resource": { + "resourceType": "Immunization", + "id": "191f288a-17f3-4cd5-a33c-a52aade6473c", + "meta": { + "versionId": "1" + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine" + } + ] + } + } + ], + "identifier": [ + { + "use": "official", + "system": "https://supplierABC/identifiers/vacc", + "value": "e2154d29-1ead-4830-a513-0d59705078fa" + } + ], + "status": "completed", + "vaccineCode": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)" + } + ] + }, + "patient": { + "reference": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "type": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449306206" + } + }, + "occurrenceDateTime": "2021-02-07T13:28:17.271000+00:00", + "recorded": "2021-02-07T13:28:17.271000+00:00", + "primarySource": true, + "location": { + "identifier": { + "system": "urn:iso:std:iso:3166", + "value": "GB" + } + }, + "manufacturer": { + "display": "AstraZeneca Ltd" + }, + "lotNumber": "4120Z001", + "expirationDate": "2021-07-02", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure (body structure)" + } + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular route (qualifier value)" + } + ] + }, + "doseQuantity": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "ml" + }, + "performer": [ + { + "actor": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "B0C4P" + }, + "display": "UNIVERSITY HOSPITAL OF WALES" + } + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "443684005", + "display": "Disease outbreak (event)" + } + ] + } + ], + "protocolApplied": [ + { + "targetDisease": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "55735004", + "display": "Respiratory syncytial virus infection (disorder)" + } + ] + } + ], + "doseNumberPositiveInt": 1 + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac", + "resource": { + "resourceType": "Patient", + "id": "9449306206", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449306206" + } + ], + "birthDate": "2014-03-25" + }, + "search": { + "mode": "include" + } + } + ], + "total": 1 + } + }, + "OperationOutcome": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/Narrative", + "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety." + }, + "contained": { + "type": "array", + "items": { + "description": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "type": "object", + "discriminator": { + "propertyName": "resourceType" + }, + "properties": { + "resourceType": { + "type": "string", + "example": "OperationOutcome" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "issue": { + "type": "array", + "items": { + "type": "object", + "properties": { + "severity": { + "type": "string" + }, + "code": { + "type": "string" + }, + "details": { + "type": "object", + "properties": { + "coding": { + "type": "array", + "items": { + "type": "object", + "properties": { + "system": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + }, + "diagnostics": { + "type": "string" + } + } + } + } + }, + "required": ["resourceType"] + } + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + }, + "issue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OperationOutcome_Issue", + "description": "An error, warning, or information message that results from a system action." + }, + "minItems": 1 + } + }, + "required": ["issue"], + "example": { + "resourceType": "OperationOutcome", + "meta": { + "versionId": "BnpJOa5-Sb", + "lastUpdated": "2021-04-12T14:34:36.061-05:00", + "source": "BCL3d5NERb", + "profile": ["xSempdez3Y"], + "security": [ + { + "system": "tczS7uP8XL", + "version": "IXKbCw05qO", + "code": "NvDP1hL64Y", + "display": "_r1z5oJld1", + "userSelected": true + } + ], + "tag": [ + { + "system": "2qqXHsE1Mx", + "version": "lybFyQ1tBj", + "code": "Q9w075fYd3", + "display": "Nm2QqbYibP", + "userSelected": true + }, + { + "code": "ibm/complete-mock" + } + ] + }, + "implicitRules": "l8KHk6qOt4", + "language": "en-US", + "text": { + "status": "additional", + "div": "
" + }, + "issue": [ + { + "severity": "warning", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "eQgFofRHmJ", + "version": "T524HDk5Za", + "code": "tC_7iQg31j", + "display": "s0bLc4W5KE", + "userSelected": true + } + ], + "text": "BfBVppHmsh" + }, + "diagnostics": "EcvPDGbK0q", + "location": ["GK5ihTmfe6"], + "expression": ["Uidx_swV4Z"] + } + ] + } + }, + "Bundle_Entry": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "link": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bundle_Link", + "description": "A series of links that provide context to this entry." + } + }, + "fullUrl": { + "type": "string", + "pattern": "\\S*", + "description": "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource - i.e. if the fullUrl is not a urn:uuid, the URL shall be version-independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified." + }, + "resource": { + "description": "The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type." + }, + "resourceType": { + "type": "string", + "example": "Immunization" + }, + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + "meta": { + "$ref": "#/components/schemas/Meta", + "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource." + }, + "implicitRules": { + "type": "string", + "pattern": "\\S*", + "description": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The base language in which the resource is written." + }, + "search": { + "$ref": "#/components/schemas/Bundle_Entry_Search", + "description": "Information about the search process that lead to the creation of this entry." + }, + "request": { + "$ref": "#/components/schemas/Bundle_Entry_Request", + "description": "Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry." + }, + "response": { + "$ref": "#/components/schemas/Bundle_Entry_Response", + "description": "Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history." + } + } + } + ] + }, + "Bundle_Entry_Response": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The status code returned by processing this entry. The status SHALL start with a 3 digit HTTP code (e.g. 404) and may contain the standard HTTP description associated with the status code." + }, + "location": { + "type": "string", + "pattern": "\\S*", + "description": "The location header created by processing this operation, populated if the operation returns a location." + }, + "etag": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The Etag for the resource, if the operation for the entry produced a versioned resource (see [Resource Metadata and Versioning](http.html#versioning) and [Managing Resource Contention](http.html#concurrency))." + }, + "lastModified": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "The date/time that the resource was modified on the server." + }, + "outcome": { + "$ref": "#/components/schemas/Resource", + "description": "An OperationOutcome containing hints and warnings produced as part of processing this entry in a batch or transaction." + } + }, + "required": ["status"] + } + ] + }, + "Bundle_Entry_Request": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "method": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "In a transaction or batch, this is the HTTP action to be executed for this entry. In a history bundle, this indicates the HTTP action that occurred." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "The URL for this entry, relative to the root (the address to which the request is posted)." + }, + "ifNoneMatch": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "If the ETag values match, return a 304 Not Modified status. See the API documentation for [\"Conditional Read\"](http.html#cread)." + }, + "ifModifiedSince": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "Only perform the operation if the last updated date matches. See the API documentation for [\"Conditional Read\"](http.html#cread)." + }, + "ifMatch": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Only perform the operation if the Etag value matches. For more information, see the API section [\"Managing Resource Contention\"](http.html#concurrency)." + }, + "ifNoneExist": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Instruct the server not to perform the create if a specified resource already exists. For further information, see the API documentation for [\"Conditional Create\"](http.html#ccreate). This is just the query portion of the URL - what follows the \"?\" (not including the \"?\")." + } + }, + "required": ["method", "url"] + } + ] + }, + "Bundle_Entry_Search": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "mode": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Why this entry is in the result set - whether it's included as a match or because of an _include requirement, or to convey information or warning information about the search process." + }, + "score": { + "type": "number", + "description": "When searching, the server's search ranking score for the entry." + } + } + } + ] + }, + "Bundle_Link": { + "allOf": [ + { + "type": "object", + "properties": { + "relation": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A name which details the functional use for this link - see [http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1](http://www.iana.org/assignments/link-relations/link-relations.xhtml#link-relations-1)." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "The reference details for the link." + } + }, + "required": ["relation", "url"] + } + ] + }, + "Immunization_ProtocolApplied": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "series": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "One possible path to achieve presumed immunity against a disease - within the context of an authority." + }, + "authority": { + "$ref": "#/components/schemas/Reference", + "description": "Indicates the authority who published the protocol (e.g. ACIP) that is being followed." + }, + "targetDisease": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "The vaccine preventable disease the dose is being administered against." + } + }, + "doseNumberPositiveInt": { + "type": "integer", + "format": "int32", + "description": "Nominal position in a series." + }, + "doseNumberString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Nominal position in a series." + }, + "seriesDosesPositiveInt": { + "type": "integer", + "format": "int32", + "description": "The recommended number of doses to achieve immunity." + }, + "seriesDosesString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The recommended number of doses to achieve immunity." + } + } + } + ] + }, + "Immunization_Reaction": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "date": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date of reaction to the immunization." + }, + "detail": { + "$ref": "#/components/schemas/Reference", + "description": "Details of the reaction." + }, + "reported": { + "type": "boolean", + "description": "Self-reported indicator." + } + } + } + ] + }, + "Immunization_Education": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "documentType": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Identifier of the material presented to the patient." + }, + "reference": { + "type": "string", + "pattern": "\\S*", + "description": "Reference pointer to the educational material given to the patient if the information was on line." + }, + "publicationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date the educational material was published." + }, + "presentationDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Date the educational material was given to the patient." + } + } + } + ] + }, + "Immunization_Performer": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "function": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Describes the type of performance (e.g. ordering provider, administering provider, etc.)." + }, + "actor": { + "$ref": "#/components/schemas/Reference", + "description": "The practitioner or organization who performed the action." + } + }, + "required": ["actor"] + } + ] + }, + "OperationOutcome_Issue": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "severity": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Indicates whether the issue indicates a variation from successful processing." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." + }, + "details": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Additional details about the error. This may be a text description of the error or a system code that identifies the error." + }, + "diagnostics": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Additional diagnostic information about the issue." + }, + "location": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "This element is deprecated because it is XML specific. It is replaced by issue.expression, which is format independent, and simpler to parse. \n\nFor resource issues, this will be a simple XPath limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised. For HTTP errors, will be \"http.\" + the parameter name." + } + }, + "expression": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A [simple subset of FHIRPath](fhirpath.html#simple) limited to element names, repetition indicators and the default child accessor that identifies one of the elements in the resource that caused this issue to be raised." + } + } + }, + "required": ["severity", "code"] + } + ] + }, + "Element": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + }, + "example": [ + { + "url": "http://example.com", + "valueString": "text value" + } + ] + } + } + }, + "BackboneElement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "modifierExtension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself)." + } + } + } + }, + "Address": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The purpose of this address." + }, + "type": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Distinguishes between physical addresses (those you can visit) and mailing addresses (e.g. PO Boxes and care-of addresses). Most addresses are both." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Specifies the entire address as it should be displayed e.g. on a postal label. This may be provided instead of or as well as the specific parts." + }, + "line": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "This component contains the house number, apartment number, street name, street direction, P.O. Box number, delivery hints, and similar address information." + } + }, + "city": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of the city, town, suburb, village or other community or delivery center." + }, + "district": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of the administrative area (county)." + }, + "state": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Sub-unit of a country with limited sovereignty in a federally organized country. A code may be used if codes are in common use (e.g. US 2 letter state codes)." + }, + "postalCode": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A postal code designating a region defined by the postal service." + }, + "country": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Country - a nation as commonly understood or generally accepted." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period when address was/is in use." + } + } + } + ] + }, + "Age": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Annotation": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "authorReference": { + "$ref": "#/components/schemas/Reference", + "description": "The individual responsible for making the annotation." + }, + "authorString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The individual responsible for making the annotation." + }, + "time": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Indicates when this particular annotation was made." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The text of the annotation in markdown format." + } + }, + "required": ["text"] + } + ] + }, + "Attachment": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate." + }, + "language": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The human language of the content. The value can be any valid value according to BCP 47." + }, + "data": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The actual data of the attachment - a sequence of bytes, base64 encoded." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "A location where the data can be accessed." + }, + "size": { + "type": "integer", + "format": "int32", + "description": "The number of bytes of data that make up this attachment (before base64 encoding, if that is done)." + }, + "hash": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The calculated hash of the data using SHA-1. Represented using base64." + }, + "title": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A label or set of text to display in place of the data." + }, + "creation": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The date that the attachment was first created." + } + } + } + ] + }, + "CodeableConcept": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "coding": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "A reference to a code defined by a terminology system." + } + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user." + } + } + }, + "Coding": { + "type": "object", + "properties": { + "system": { + "type": "string", + "pattern": "\\S*", + "description": "The identification of the code system that defines the meaning of the symbol in the code." + }, + "version": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination)." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A representation of the meaning of the code in the system, following the rules of the system." + }, + "userSelected": { + "type": "boolean", + "description": "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays)." + } + } + }, + "ContactPoint": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "system": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Telecommunications form for contact point - what communications system is required to make use of the contact." + }, + "value": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The actual contact point details, in a form that is meaningful to the designated communication system (i.e. phone number or email address)." + }, + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the purpose for the contact point." + }, + "rank": { + "type": "integer", + "format": "int32", + "description": "Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period when the contact point was/is in use." + } + } + } + ] + }, + "Count": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Distance": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Duration": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "HumanName": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Identifies the purpose for this name." + }, + "text": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts." + }, + "family": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father." + }, + "given": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Given name." + } + }, + "prefix": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name." + } + }, + "suffix": { + "type": "array", + "items": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name." + } + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Indicates the period of time when this name was valid for the named person." + } + } + } + ] + }, + "Identifier": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "use": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The purpose of this identifier." + }, + "type": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A coded type for the identifier that can be used to determine which identifier to use for a specific purpose." + }, + "system": { + "type": "string", + "pattern": "\\S*", + "description": "Establishes the namespace for the value - that is, a URL that describes a set values that are unique." + }, + "value": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The portion of the identifier typically relevant to the user and which is unique within the context of the system." + }, + "period": { + "$ref": "#/components/schemas/Period", + "description": "Time period during which identifier is/was valid for use." + }, + "assigner": { + "$ref": "#/components/schemas/Reference", + "description": "Organization that issued/manages the identifier.", + "example": { + "reference": "Organization/123", + "type": "Organization", + "display": "The Assigning Organization" + } + } + } + }, + "Money": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "value": { + "type": "number", + "description": "Numerical value (with implicit precision)." + }, + "currency": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "ISO 4217 Currency Code." + } + } + } + ] + }, + "MoneyQuantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Period": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "start": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The start of the period. The boundary is inclusive." + }, + "end": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time." + } + } + } + ] + }, + "Quantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "value": { + "type": "number", + "description": "The value of the measured amount. The value includes an implicit precision in the presentation of the value." + }, + "comparator": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "How the value should be understood and represented - whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is \"<\" , then the real value is < stated value." + }, + "unit": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A human-readable form of the unit." + }, + "system": { + "type": "string", + "pattern": "\\S*", + "description": "The identification of the system that provides the coded form of the unit." + }, + "code": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A computer processable form of the unit in some unit representation system." + } + } + } + ] + }, + "Range": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "low": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The low limit. The boundary is inclusive." + }, + "high": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The high limit. The boundary is inclusive." + } + } + } + ] + }, + "Ratio": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "numerator": { + "$ref": "#/components/schemas/Quantity", + "description": "The value of the numerator." + }, + "denominator": { + "$ref": "#/components/schemas/Quantity", + "description": "The value of the denominator." + } + } + } + ] + }, + "Reference": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "reference": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources." + }, + "type": { + "type": "string", + "pattern": "\\S*", + "description": "The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent.\n\nThe type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. \"Patient\" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources)." + }, + "identifier": { + "$ref": "#/components/schemas/Identifier", + "description": "An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Plain text narrative that identifies the resource in addition to the resource reference." + } + } + }, + "SampledData": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "origin": { + "$ref": "#/components/schemas/SimpleQuantity", + "description": "The base quantity that a measured value of zero represents. In addition, this provides the units of the entire measurement series." + }, + "period": { + "type": "number", + "description": "The length of time between sampling times, measured in milliseconds." + }, + "factor": { + "type": "number", + "description": "A correction factor that is applied to the sampled data points before they are added to the origin." + }, + "lowerLimit": { + "type": "number", + "description": "The lower limit of detection of the measured points. This is needed if any of the data points have the value \"L\" (lower than detection limit)." + }, + "upperLimit": { + "type": "number", + "description": "The upper limit of detection of the measured points. This is needed if any of the data points have the value \"U\" (higher than detection limit)." + }, + "dimensions": { + "type": "integer", + "format": "int32", + "description": "The number of sample points at each time point. If this value is greater than one, then the dimensions will be interlaced - all the sample points for a point in time will be recorded at once." + }, + "data": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A series of data points which are decimal values separated by a single space (character u20). The special values \"E\" (error), \"L\" (below detection limit) and \"U\" (above detection limit) can also be used in place of a decimal value." + } + }, + "required": ["origin", "period", "dimensions"] + } + ] + }, + "SimpleQuantity": { + "allOf": [ + { + "$ref": "#/components/schemas/Quantity" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "Signature": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "type": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "An indication of the reason that the entity signed this document. This may be explicitly included as part of the signature information and can be used when determining accountability for various actions concerning the document." + }, + "minItems": 1 + }, + "when": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the digital signature was signed." + }, + "who": { + "$ref": "#/components/schemas/Reference", + "description": "A reference to an application-usable description of the identity that signed (e.g. the signature used their private key)." + }, + "onBehalfOf": { + "$ref": "#/components/schemas/Reference", + "description": "A reference to an application-usable description of the identity that is represented by the signature." + }, + "targetFormat": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A mime type that indicates the technical format of the target resources signed by the signature." + }, + "sigFormat": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "A mime type that indicates the technical format of the signature. Important mime types are application/signature+xml for X ML DigSig, application/jose for JWS, and image/* for a graphical image of a signature, etc." + }, + "data": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "The base64 encoding of the Signature content. When signature is not recorded electronically this element would be empty." + } + }, + "required": ["type", "when", "who"] + } + ] + }, + "Timing": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "event": { + "type": "array", + "items": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Identifies specific times when the event occurs." + } + }, + "repeat": { + "$ref": "#/components/schemas/Timing_Repeat", + "description": "A set of rules that describe when the event is scheduled." + }, + "code": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A code for the timing schedule (or just text in code.text). Some codes such as BID are ubiquitous, but many institutions define their own additional codes. If a code is provided, the code is understood to be a complete statement of whatever is specified in the structured timing data, and either the code or the data may be used to interpret the Timing, with the exception that .repeat.bounds still applies over the code (and is not contained in the code)." + } + } + } + ] + }, + "Timing_Repeat": { + "allOf": [ + { + "$ref": "#/components/schemas/BackboneElement" + }, + { + "type": "object", + "properties": { + "boundsDuration": { + "$ref": "#/components/schemas/Duration", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "boundsRange": { + "$ref": "#/components/schemas/Range", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "boundsPeriod": { + "$ref": "#/components/schemas/Period", + "description": "Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule." + }, + "count": { + "type": "integer", + "format": "int32", + "description": "A total count of the desired number of repetitions across the duration of the entire timing specification. If countMax is present, this element indicates the lower bound of the allowed range of count values." + }, + "countMax": { + "type": "integer", + "format": "int32", + "description": "If present, indicates that the count is a range - so to perform the action between [count] and [countMax] times." + }, + "duration": { + "type": "number", + "description": "How long this thing happens for when it happens. If durationMax is present, this element indicates the lower bound of the allowed range of the duration." + }, + "durationMax": { + "type": "number", + "description": "If present, indicates that the duration is a range - so to perform the action between [duration] and [durationMax] time length." + }, + "durationUnit": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The units of time for the duration, in UCUM units." + }, + "frequency": { + "type": "integer", + "format": "int32", + "description": "The number of times to repeat the action within the specified period. If frequencyMax is present, this element indicates the lower bound of the allowed range of the frequency." + }, + "frequencyMax": { + "type": "integer", + "format": "int32", + "description": "If present, indicates that the frequency is a range - so to repeat between [frequency] and [frequencyMax] times within the period or period range." + }, + "period": { + "type": "number", + "description": "Indicates the duration of time over which repetitions are to occur; e.g. to express \"3 times per day\", 3 would be the frequency and \"1 day\" would be the period. If periodMax is present, this element indicates the lower bound of the allowed range of the period length." + }, + "periodMax": { + "type": "number", + "description": "If present, indicates that the period is a range from [period] to [periodMax], allowing expressing concepts such as \"do this once every 3-5 days." + }, + "periodUnit": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The units of time for the period in UCUM units." + }, + "dayOfWeek": { + "type": "array", + "items": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "If one or more days of week is provided, then the action happens only on the specified day(s)." + } + }, + "timeOfDay": { + "type": "array", + "items": { + "type": "string", + "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", + "description": "Specified time of day for action to take place." + } + }, + "when": { + "type": "array", + "items": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "An approximate time period during the day, potentially linked to an event of daily living that indicates when the action should occur." + } + }, + "offset": { + "type": "integer", + "format": "int32", + "description": "The number of minutes from the event. If the event code does not indicate whether the minutes is before or after the event, then the offset is assumed to be after the event." + } + } + } + ] + }, + "ContactDetail": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "The name of an individual to contact." + }, + "telecom": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContactPoint", + "description": "The contact details for the individual (if a name was provided) or the organization." + } + } + } + } + ] + }, + "RelatedArtifact": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The type of relationship to the related artifact." + }, + "label": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A short label that can be used to reference the citation from elsewhere in the containing artifact, such as a footnote index." + }, + "display": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A brief description of the document or knowledge resource being referenced, suitable for display to a consumer." + }, + "citation": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "A bibliographic citation for the related artifact. This text SHOULD be formatted according to an accepted citation format." + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "A url for the artifact that can be followed to access the actual content." + }, + "document": { + "$ref": "#/components/schemas/Attachment", + "description": "The document being referenced, represented as an attachment. This is exclusive with the resource element." + }, + "resource": { + "type": "string", + "pattern": "\\S*", + "description": "The related resource, such as a library, value set, profile, or other knowledge resource." + } + }, + "required": ["type"] + } + ] + }, + "UsageContext": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/Coding", + "description": "A code that identifies the type of context being specified by this usage context." + }, + "valueCodeableConcept": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueQuantity": { + "$ref": "#/components/schemas/Quantity", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueRange": { + "$ref": "#/components/schemas/Range", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + }, + "valueReference": { + "$ref": "#/components/schemas/Reference", + "description": "A value that defines the context specified in this context of use. The interpretation of the value is defined by the code." + } + }, + "required": ["code"] + } + ] + }, + "Meta": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "The version specific identifier, as it appears in the version portion of the URL. This value changes when the resource is created, updated, or deleted." + }, + "lastUpdated": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "When the resource last changed - e.g. when the version changed." + }, + "source": { + "type": "string", + "pattern": "\\S*", + "description": "A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc." + }, + "profile": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\S*", + "description": "A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition-definitions.html#StructureDefinition.url)." + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure." + } + }, + "tag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coding", + "description": "Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource." + } + } + }, + "required": ["versionId"] + } + ] + }, + "Narrative": { + "allOf": [ + { + "$ref": "#/components/schemas/Element" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "The status of the narrative - whether it's entirely generated (from just the defined data or the extensions too), or whether a human authored it and it may contain additional data." + }, + "div": { + "type": "string", + "description": "The actual narrative content, a stripped down version of XHTML." + } + }, + "required": ["status", "div"] + } + ] + }, + "Extension": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + }, + "extension": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extension", + "description": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension." + } + }, + "url": { + "type": "string", + "pattern": "\\S*", + "description": "Source of the definition for the extension code - a logical name or a URL." + }, + "valueBase64Binary": { + "type": "string", + "pattern": "(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueBoolean": { + "type": "boolean", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCanonical": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCode": { + "type": "string", + "pattern": "[^\\s]+(\\s[^\\s]+)*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDate": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDateTime": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDecimal": { + "type": "number", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueId": { + "type": "string", + "pattern": "[A-Za-z0-9\\-\\.]{1,64}", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueInstant": { + "type": "string", + "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueInteger": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMarkdown": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueOid": { + "type": "string", + "pattern": "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valuePositiveInt": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueString": { + "type": "string", + "pattern": "[ \\r\\n\\t\\S]+", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueTime": { + "type": "string", + "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUnsignedInt": { + "type": "integer", + "format": "int32", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUri": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUrl": { + "type": "string", + "pattern": "\\S*", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUuid": { + "type": "string", + "pattern": "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAddress": { + "$ref": "#/components/schemas/Address", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAge": { + "$ref": "#/components/schemas/Age", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAnnotation": { + "$ref": "#/components/schemas/Annotation", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueAttachment": { + "$ref": "#/components/schemas/Attachment", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCodeableConcept": { + "$ref": "#/components/schemas/CodeableConcept", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCoding": { + "$ref": "#/components/schemas/Coding", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueContactPoint": { + "$ref": "#/components/schemas/ContactPoint", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueCount": { + "$ref": "#/components/schemas/Count", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDistance": { + "$ref": "#/components/schemas/Distance", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueDuration": { + "$ref": "#/components/schemas/Duration", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueHumanName": { + "$ref": "#/components/schemas/HumanName", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueIdentifier": { + "$ref": "#/components/schemas/Identifier", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMoney": { + "$ref": "#/components/schemas/Money", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valuePeriod": { + "$ref": "#/components/schemas/Period", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueQuantity": { + "$ref": "#/components/schemas/Quantity", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRange": { + "$ref": "#/components/schemas/Range", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRatio": { + "$ref": "#/components/schemas/Ratio", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueReference": { + "$ref": "#/components/schemas/Reference", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueSampledData": { + "$ref": "#/components/schemas/SampledData", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueSignature": { + "$ref": "#/components/schemas/Signature", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueTiming": { + "$ref": "#/components/schemas/Timing", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueContactDetail": { + "$ref": "#/components/schemas/ContactDetail", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueRelatedArtifact": { + "$ref": "#/components/schemas/RelatedArtifact", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueUsageContext": { + "$ref": "#/components/schemas/UsageContext", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + }, + "valueMeta": { + "$ref": "#/components/schemas/Meta", + "description": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list)." + } + }, + "required": ["url"] + } + } + } +} diff --git a/specification/immunisation-fhir-api.yaml b/specification/immunisation-fhir-api.yaml index 99f57597a..5d5d5d50b 100644 --- a/specification/immunisation-fhir-api.yaml +++ b/specification/immunisation-fhir-api.yaml @@ -26,31 +26,31 @@ info: - Vaccination events for the disease types found in this list {To be confirm} ### Data availability, timing and quality - + This is a real time service, constrained by the time taken for providers to transfer vaccination events. In most cases the latest a record will be available is within 48 hours of the immunisation event. - + The API search interaction will only return immunisation records based on a traced NHS number. Other interactions require the immunisation ID assigned by the API to interact with individual records for read, update and delete. The vaccination events for all disease types are limited to vaccinations administered on behalf of NHS England. - + There is a limited scope of data validation upon receipt of the data. Whilst the data is generally of a good, reliable quality, consumers must be aware data is shared as received and users should consider the risk of potential absences or inaccuracies of the data. ## Who can use this API This API can only be used where there is a commercial, legal and clinical basis to do so. Make sure you have a valid use case before you go too far with your development. - + You must demonstrate you have a valid use case as part of digital onboarding. As this service is disease type agnostic, there is also a shortened onboarding process for each disease type, to explain the legal, commercial and clinical justifications. - + You must do this before you can go live refer to the [Onboarding](https://digital.nhs.uk/developer/api-catalogue/immunisation-fhir-api#overview--onboarding). - + ## Who can access immunisation event records Health and care organisations in England can access immunisation records. - + Legitimate direct care examples include NHS organisations delivering healthcare, local authorities delivering care, third sector and private sector health and care organisations, and developers delivering systems to health and care organisations. - + A future capability will be to enable patients who receive health and social care or make use of NHS services in England to access their vaccination records. - + The immunisation event records captured via the API are used for a number of purposes including (some uses include distribution of the data by means other than this API) - supporting a patient getting a vaccination @@ -60,7 +60,7 @@ info: - supporting the booking of a vaccination appointment - supporting the payment reconciliation process on behalf of NHS England / NHS Business Services Authority - supporting measuring the overall success of a vaccination campaign to help inform the success of the [NHS Vaccination Strategy](https://www.england.nhs.uk/long-read/nhs-vaccination-strategy/) - + ## API status and roadmap This API is [in development](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#statuses). @@ -107,7 +107,7 @@ info: To use this access mode, use the following security pattern: - [Application-restricted RESTful API - signed JWT authentication](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication) - + Access may be restricted to certain functionality based on your user need contact: @@ -117,7 +117,7 @@ info: url: 'https://digital.nhs.uk/developer/help-and-support' email: api.management@nhs.net - + ## Errors We use standard HTTP status codes to show whether an API request succeeded or not. They are usually in the range: @@ -126,7 +126,7 @@ info: * 500 to 599 if it failed because of an error on our server Errors specific to each API are shown in the Endpoints section, under Response. See our [reference guide](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#http-status-codes) for more on errors. - + ## Open source You might find the following [open source](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#open-source) resources useful: @@ -163,29 +163,29 @@ info: * is for formal integration testing * is stateful, so persists updates * includes authorisation, with options for user-restricted access (with or without [smartcards](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/nhs-smartcards-for-developers)) and application-restricted access - + For read-only testing, we will provide an Immunisation records test pack soon. To test creating, updating and deleting patient vaccination events, you must set up your own test data. For more details see [integration testing with our RESTful APIs](https://digital.nhs.uk/developer/guides-and-documentation/testing#integration-testing-with-our-restful-apis). - + ## Onboarding You need to get your software approved by us before it can go live with this API. We call this onboarding. The onboarding process can sometimes be quite long, so it is worth planning well ahead. - + Whilst this API is in Alpha it is not possible to onboard to this API. As part of this process, you need to demonstrate that you can manage risks and that your software conforms technically with the requirements for this API. Information on this page might impact the design of your software. - + To understand how our online digital onboarding process works, see [digital onboarding](https://digital.nhs.uk/developer/guides-and-documentation/digital-onboarding#using-the-digital-onboarding-portal). servers: - - url: 'https://sandbox.api.service.nhs.uk/immunisation-fhir-api' + - url: "https://sandbox.api.service.nhs.uk/immunisation-fhir-api" description: Sandbox Server - - url: 'https://int.api.service.nhs.uk/immunisation-fhir-api' + - url: "https://int.api.service.nhs.uk/immunisation-fhir-api" description: Integration Server paths: /Immunization: @@ -193,22 +193,22 @@ paths: summary: Record a vaccination given to a patient - yet to be released operationId: createImmunization description: | - ## Overview - Use this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the 'disease types' enabled in this interaction and represented by the correct SNOMED concept(s) for that 'disease type'. See another page for details and disease types and coding. - You must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. + ## Overview + Use this interaction to record the administration of a vaccination. The immunization resource must include a targetDisease(s) matching the 'disease types' enabled in this interaction and represented by the correct SNOMED concept(s) for that 'disease type'. See another page for details and disease types and coding. + You must be authorised for the create interaction and the disease type associated with the vaccination event in order to submit a new record. - ## Sandbox testing + ## Sandbox testing - | Scenario | Request | Response | - | ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------| - | | | | + | Scenario | Request | Response | + | ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------| + | | | | parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" requestBody: type: object - $ref: '#/components/requestBodies/Immunization' + $ref: "#/components/requestBodies/Immunization" responses: "201": description: Create Immunization operation successful @@ -216,7 +216,7 @@ paths: Location: $ref: components/schemas/Location.yaml 4XX: - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" get: summary: Search (GET) for a patient's immunisation records operationId: searchViaGetImmunization @@ -235,15 +235,15 @@ paths: | | | | | Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\|9000000009` | HTTP Status 200 with immunisation data in response body | | Bad Request | Didn't pass Required fields `patient.identifier` or `-immunization.target` | HTTP Status 400 Bad Request | - + parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' - - $ref: '#/components/parameters/PatientIdentifier' - - $ref: '#/components/parameters/ImmunizationTarget' - - $ref: '#/components/parameters/DateFrom' - - $ref: '#/components/parameters/DateTo' - - $ref: '#/components/parameters/Include' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" + - $ref: "#/components/parameters/PatientIdentifier" + - $ref: "#/components/parameters/ImmunizationTarget" + - $ref: "#/components/parameters/DateFrom" + - $ref: "#/components/parameters/DateTo" + - $ref: "#/components/parameters/Include" responses: "200": @@ -251,9 +251,9 @@ paths: content: application/fhir+json: schema: - $ref: '#/components/schemas/Bundle' + $ref: "#/components/schemas/Bundle" "400": - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" /Immunization/_search: post: summary: Search (POST) for a patient's immunisation records @@ -261,7 +261,7 @@ paths: description: | ## Overview You may use this interaction as an alternative to a search with the GET verb. A POST search allows you to supply some or all parameters in the body of the request should you need to do so. It offers the same search functionality, see Search (GET) interaction for details. - + ## Sandbox testing You can test the following scenarios in our sandbox environment: @@ -270,29 +270,29 @@ paths: | | | | | Immunisation history found | `patient.identifier`=`https://fhir.nhs.uk/Id/nhs-number\|9000000009` | HTTP Status 200 with immunisation data in response body | | Bad Request | Didn't pass Required fields `patient.identifier` or -immunization.target or _include | HTTP Status 400 Bad Request | - + parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' - - $ref: '#/components/parameters/PatientIdentifier' - - $ref: '#/components/parameters/ImmunizationTarget' - - $ref: '#/components/parameters/DateFrom' - - $ref: '#/components/parameters/DateTo' - - $ref: '#/components/parameters/Include' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" + - $ref: "#/components/parameters/PatientIdentifier" + - $ref: "#/components/parameters/ImmunizationTarget" + - $ref: "#/components/parameters/DateFrom" + - $ref: "#/components/parameters/DateTo" + - $ref: "#/components/parameters/Include" requestBody: - $ref: '#/components/requestBodies/SearchImmunization' + $ref: "#/components/requestBodies/SearchImmunization" responses: "200": description: Search immunisation operation successful content: application/fhir+json: schema: - $ref: '#/components/schemas/Bundle' + $ref: "#/components/schemas/Bundle" "400": - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" /Immunization/{id}: get: - summary: Retrieve a record of an immunisation by its unique identifier + summary: Retrieve a record of an immunisation by its unique identifier operationId: readImmunization description: | ## Overview @@ -300,7 +300,7 @@ paths: The response will include an eTag for the version of the record which has been returned. If you intend to update a record, it is recommended that you use this interaction to obtain the latest version (and eTag for the version). To retrieve a full vaccination history for a patient, see the search interaction. You must be authorised for the read interaction and the disease type associated with the vaccination event in order to access the record. - + ## Sandbox testing You can test the following scenarios in our sandbox environment: @@ -310,24 +310,23 @@ paths: | Immunisation record found | `id`=`12a33650-6f94-4e8f-a971-1c5c41da5b22` | HTTP Status 200 with immunisation data in response body | | Bad Request | Didn't pass Required fields `id` | HTTP Status 400 Bad Request | - parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' - - $ref: '#/components/parameters/Id' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" + - $ref: "#/components/parameters/Id" responses: "200": description: Read Immunization operation successful content: application/fhir+json: schema: - $ref: '#/components/schemas/Immunization' + $ref: "#/components/schemas/Immunization" "400": - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" content: application/fhir+json: schema: - $ref: '#/components/schemas/OperationOutcome' + $ref: "#/components/schemas/OperationOutcome" example: resourceType: OperationOutcome id: 3d64df5a-b753-49ec-b3df-f45c157941eb @@ -336,7 +335,7 @@ paths: code: invalid details: coding: - - system: 'https://fhir.nhs.uk/Codesystem/http-error-codes' + - system: "https://fhir.nhs.uk/Codesystem/http-error-codes" code: INVALID diagnostics: The provided event ID is either missing or not in the expected format. put: @@ -355,14 +354,13 @@ paths: | ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------| | | | | - parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' - - $ref: '#/components/parameters/Id' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" + - $ref: "#/components/parameters/Id" requestBody: type: object - $ref: '#/components/requestBodies/Immunization' + $ref: "#/components/requestBodies/Immunization" responses: "200": description: Update Immunization operation successful @@ -375,7 +373,7 @@ paths: Location: $ref: components/schemas/Location.yaml 4XX: - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" delete: summary: Mark a record of vaccination as being entered in error - `yet to be released` operationId: deleteImmunization @@ -391,12 +389,10 @@ paths: | ----------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------| | | | | - - parameters: - - $ref: '#/components/parameters/CorrelationID' - - $ref: '#/components/parameters/RequestID' - - $ref: '#/components/parameters/Id' + - $ref: "#/components/parameters/CorrelationID" + - $ref: "#/components/parameters/RequestID" + - $ref: "#/components/parameters/Id" responses: "204": description: Delete Immunization operation successful @@ -404,7 +400,7 @@ paths: Location: $ref: components/schemas/Location.yaml 4XX: - $ref: '#/components/responses/4XX-imms' + $ref: "#/components/responses/4XX-imms" components: responses: @@ -412,30 +408,30 @@ components: description: > Below are examples of potential HTTP status codes and their associated error codes, which could be returned in the event of a fault. - - + + | HTTP status | Error code | Description | Example | - + | ----------- | -------------------------- | --------------------------------------------- |--------------------------------------------------------------------------------------| - + | 400 | INVALID | The provided event ID is either missing or not in the expected format. | {"resourceType": "OperationOutcome", "id": "6f4ca309-19d7-4f61-90b3-acbd1f2eb8f8", "meta": {"profile": ["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"]}, "issue": [{"severity": "error", "code": "invalid", "details": {"coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}]}, "diagnostics": "the provided event ID is either missing or not in the expected format."}]} | - + | 400 | BAD_REQUEST | Search could not be processed or failed basic FHIR validation rules | {"resourceType": "OperationOutcome", "id": "4ff75db4-6e7d-411d-a490-c55c11f83043", "meta": {"profile": ["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"]}, "issue": [{"severity": "error", "code": "invalid", "details": {"coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "INVALID"}]}, "diagnostics": "patient.identifier must be in the format of \"https://fhir.nhs.uk/Id/nhs-number|{NHS number}\" e.g. \"https://fhir.nhs.uk/Id/nhs-number|9000000009\""}]} | - + | 401 | UNAUTHORISED | Authorization is required for the interaction that was attempted | {"resourceType":"OperationOutcome","id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d","meta":{"profile":["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"]},"issue":[{"severity":"error","code":"expired","details":{"coding":[{"system":"https://fhir.nhs.uk/Codesystem/http-error-codes","code":"SEND_UNAUTHORIZED"}]},"diagnostics":"The sender has not provided a token or it has expired or is otherwise invalid."}]} | | 403 | UNAUTHORISED | The sender does not have permissions to access this resource | {"resourceType":"OperationOutcome","id":"a5abca2a-4eda-41da-b2cc-95d48c6b791d","meta":{"profile":["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"]},"issue":[{"severity":"error","code":"forbidden","details":{"coding":[{"system":"https://fhir.nhs.uk/Codesystem/http-error-codes","code":"SEND_UNAUTHORIZED"}]},"diagnostics":"The sender does not have permissions to access this resource. Please check your credentials and permissions."}]} | - + | 404 | NOT_FOUND | The requested resource was not found. | {"resourceType": "OperationOutcome", "id": "bc2c3c82-4392-4314-9d6b-a7345f82d923", "meta": {"profile": ["https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"]}, "issue": [{"severity": "error", "code": "not-found", "details": {"coding": [{"system": "https://fhir.nhs.uk/Codesystem/http-error-codes", "code": "NOT-FOUND"}]}, "diagnostics": "The requested resource was not found."}]} | - + | 405 | NOT_ALLOWED | The requested method is not allowed | - + | 422 | UNPROCESSABLE_ENTITY | The proposed resource violated applicable FHIR profiles or server business rules. This should be accompanied by an OperationOutcome resource providing additional @@ -443,7 +439,7 @@ components: content: application/fhir+json: schema: - $ref: '#/components/schemas/OperationOutcome' + $ref: "#/components/schemas/OperationOutcome" example: resourceType: OperationOutcome id: 3d64df5a-b753-49ec-b3df-f45c157941eb @@ -452,7 +448,7 @@ components: code: invalid details: coding: - - system: 'https://fhir.nhs.uk/Codesystem/http-error-codes' + - system: "https://fhir.nhs.uk/Codesystem/http-error-codes" code: INVALID diagnostics: Search parameter patient.identifier/-immunization.target must have one value. @@ -461,7 +457,7 @@ components: content: application/fhir+json: schema: - $ref: '#/components/schemas/Immunization' + $ref: "#/components/schemas/Immunization" required: true SearchImmunization: content: @@ -505,8 +501,8 @@ components: description: > A required ID which you can use to identify an Immunization event object. - - + + Mirrored back in a response header. schema: type: string @@ -519,10 +515,10 @@ components: An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters. - - + + Mirrored back in a response header. - + schema: type: string pattern: >- @@ -536,14 +532,14 @@ components: A globally unique identifier (GUID) for the request, which we use to de-duplicate repeated requests and to trace the request if you contact our helpdesk. - - + + Must be a universally unique identifier (UUID) (ideally version 4). - - + + Mirrored back in a response header. - - + + If you re-send a failed request, use the same value in this header. schema: type: string @@ -758,7 +754,7 @@ components: pattern: '[A-Za-z0-9\-\.]{1,64}' description: The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes. meta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. implicitRules: type: string @@ -774,7 +770,7 @@ components: type: object properties: text: - $ref: '#/components/schemas/Narrative' + $ref: "#/components/schemas/Narrative" description: A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it "clinically safe" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety. contained: type: array @@ -940,7 +936,7 @@ components: pattern: '[A-Za-z0-9\-\.]{1,64}' description: The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes. meta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. implicitRules: type: string @@ -955,15 +951,15 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. modifierExtension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: |- May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions. - + Modifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself). Immunization: type: object @@ -973,7 +969,7 @@ components: - patient properties: text: - $ref: '#/components/schemas/Narrative' + $ref: "#/components/schemas/Narrative" description: A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it "clinically safe" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety. contained: type: array @@ -991,7 +987,7 @@ components: pattern: '[A-Za-z0-9\-\.]{1,64}' description: The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes. meta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. implicitRules: type: string @@ -1006,36 +1002,36 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. modifierExtension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: |- May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions. - + Modifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself). identifier: type: array items: - $ref: '#/components/schemas/Identifier' + $ref: "#/components/schemas/Identifier" description: A unique identifier assigned to this immunization record. status: type: string pattern: '[^\s]+(\s[^\s]+)*' description: Indicates the current status of the immunization event. statusReason: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Indicates the reason the immunization event was not performed. vaccineCode: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Vaccine that was administered or was to be administered. patient: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: The patient who either received or did not receive the immunization. encounter: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: The visit or admission or other contact between patient and health care provider the immunization was performed as part of. occurrenceDateTime: type: string @@ -1053,13 +1049,13 @@ components: type: boolean description: An indication that the content of the record is based on information from the person who administered the vaccine. This reflects the context under which the data was originally recorded. reportOrigin: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: The source of the data when the report of the immunization event is not based on information from the person who administered the vaccine. location: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: The service delivery location where the vaccine administration occurred. manufacturer: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Name of vaccine manufacturer. lotNumber: type: string @@ -1070,33 +1066,33 @@ components: pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)? description: Date vaccine batch expires. site: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Body site where vaccine was administered. route: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: The path by which the vaccine product is taken into the body. doseQuantity: - $ref: '#/components/schemas/SimpleQuantity' + $ref: "#/components/schemas/SimpleQuantity" description: The quantity of vaccine product that was administered. performer: type: array items: - $ref: '#/components/schemas/Immunization_Performer' + $ref: "#/components/schemas/Immunization_Performer" description: Indicates who performed the immunization event. note: type: array items: - $ref: '#/components/schemas/Annotation' + $ref: "#/components/schemas/Annotation" description: Extra information about the immunization that is not conveyed by the other attributes. reasonCode: type: array items: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Reasons why the vaccine was administered. reasonReference: type: array items: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Condition, Observation or DiagnosticReport that supports why the immunization was administered. isSubpotent: type: boolean @@ -1104,30 +1100,30 @@ components: subpotentReason: type: array items: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Reason why a dose is considered to be subpotent. education: type: array items: - $ref: '#/components/schemas/Immunization_Education' + $ref: "#/components/schemas/Immunization_Education" description: Educational material presented to the patient (or guardian) at the time of vaccine administration. programEligibility: type: array items: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Indicates a patient's eligibility for a funding program. fundingSource: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Indicates the source of the vaccine actually administered. This may be different than the patient eligibility (e.g. the patient may be eligible for a publically purchased vaccine but due to inventory issues, vaccine purchased with private funds was actually administered). reaction: type: array items: - $ref: '#/components/schemas/Immunization_Reaction' + $ref: "#/components/schemas/Immunization_Reaction" description: Categorical data indicating that an adverse event is associated in time to an immunization. protocolApplied: type: array items: - $ref: '#/components/schemas/Immunization_ProtocolApplied' + $ref: "#/components/schemas/Immunization_ProtocolApplied" description: The protocol (set of recommendations) being followed by the provider who administered the dose. example: resourceType: Immunization @@ -1142,14 +1138,14 @@ components: - resourceType: Patient id: Pat1 identifier: - - system: 'https://fhir.nhs.uk/Id/nhs-number' - value: '9449310475' + - system: "https://fhir.nhs.uk/Id/nhs-number" + value: "9449310475" name: - family: Owler given: - Ozzie gender: unknown - birthDate: '1965-02-28' + birthDate: "1965-02-28" address: - postalCode: ZZ99 3CZ extension: @@ -1157,62 +1153,62 @@ components: https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure valueCodeableConcept: coding: - - system: 'http://snomed.info/sct' - code: '1303503001' + - system: "http://snomed.info/sct" + code: "1303503001" display: >- Administration of RSV (respiratory syncytial virus) vaccine status: completed vaccineCode: coding: - - system: 'http://snomed.info/sct' - code: '42605811000001109' + - system: "http://snomed.info/sct" + code: "42605811000001109" display: >- Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd) patient: - reference: '#Pat1' - occurrenceDateTime: '2021-02-07T13:28:17.271000+00:00' - recorded: '2021-02-07T13:28:17.271000+00:00' + reference: "#Pat1" + occurrenceDateTime: "2021-02-07T13:28:17.271000+00:00" + recorded: "2021-02-07T13:28:17.271000+00:00" primarySource: true location: identifier: - system: 'urn:iso:std:iso:3166' + system: "urn:iso:std:iso:3166" value: GB manufacturer: display: AstraZeneca Ltd lotNumber: 4120Z001 - expirationDate: '2021-07-02' + expirationDate: "2021-07-02" site: coding: - - system: 'http://snomed.info/sct' - code: '368208006' + - system: "http://snomed.info/sct" + code: "368208006" display: Left upper arm structure (body structure) route: coding: - - system: 'http://snomed.info/sct' - code: '78421000' + - system: "http://snomed.info/sct" + code: "78421000" display: Intramuscular route (qualifier value) doseQuantity: value: 0.5 unit: milliliter - system: 'http://unitsofmeasure.org' + system: "http://unitsofmeasure.org" code: ml performer: - actor: - reference: '#Pract1' + reference: "#Pract1" - actor: type: Organization identifier: - system: 'https://fhir.nhs.uk/Id/ods-organization-code' + system: "https://fhir.nhs.uk/Id/ods-organization-code" value: N2N9I reasonCode: - coding: - - system: 'http://snomed.info/sct' - code: '443684005' + - system: "http://snomed.info/sct" + code: "443684005" protocolApplied: - targetDisease: - coding: - - system: 'http://snomed.info/sct' - code: '55735004' + - system: "http://snomed.info/sct" + code: "55735004" display: >- Respiratory syncytial virus infection (disorder) doseNumberPositiveInt: 1 @@ -1231,25 +1227,25 @@ components: example: Bundle type: description: >- - Indicates how the bundle is intended to be used. Always - `searchset`. + Indicates how the bundle is intended to be used. Always + `searchset`. type: string example: searchset link: type: array items: - $ref: '#/components/schemas/Bundle_Link' + $ref: "#/components/schemas/Bundle_Link" description: A series of links that provide context to this bundle. entry: type: array items: - $ref: '#/components/schemas/Bundle_Entry' + $ref: "#/components/schemas/Bundle_Entry" description: An entry in a bundle resource - will either contain a resource or information about a resource (transactions and history only). total: type: integer format: int32 description: If a set of search matches, this is the total number of entries of type 'match' across all pages in the search. It does not include search.mode = 'include' or 'outcome' entries and it does not provide a count of the number of entries in the Bundle. - example: + example: resourceType: Bundle type: searchset link: @@ -1267,83 +1263,83 @@ components: https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure valueCodeableConcept: coding: - - system: 'http://snomed.info/sct' - code: '1303503001' + - system: "http://snomed.info/sct" + code: "1303503001" display: >- Administration of RSV (respiratory syncytial virus) vaccine identifier: - use: official - system: 'https://supplierABC/identifiers/vacc' + system: "https://supplierABC/identifiers/vacc" value: e2154d29-1ead-4830-a513-0d59705078fa status: completed vaccineCode: coding: - - system: 'http://snomed.info/sct' - code: '42605811000001109' + - system: "http://snomed.info/sct" + code: "42605811000001109" display: >- Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd) patient: - reference: 'urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac' + reference: "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" type: Patient identifier: - system: 'https://fhir.nhs.uk/Id/nhs-number' - value: '9449306206' - occurrenceDateTime: '2021-02-07T13:28:17.271000+00:00' - recorded: '2021-02-07T13:28:17.271000+00:00' + system: "https://fhir.nhs.uk/Id/nhs-number" + value: "9449306206" + occurrenceDateTime: "2021-02-07T13:28:17.271000+00:00" + recorded: "2021-02-07T13:28:17.271000+00:00" primarySource: true location: identifier: - system: 'urn:iso:std:iso:3166' + system: "urn:iso:std:iso:3166" value: GB manufacturer: display: AstraZeneca Ltd lotNumber: 4120Z001 - expirationDate: '2021-07-02' + expirationDate: "2021-07-02" site: coding: - - system: 'http://snomed.info/sct' - code: '368208006' + - system: "http://snomed.info/sct" + code: "368208006" display: Left upper arm structure (body structure) route: coding: - - system: 'http://snomed.info/sct' - code: '78421000' + - system: "http://snomed.info/sct" + code: "78421000" display: Intramuscular route (qualifier value) doseQuantity: value: 0.5 unit: milliliter - system: 'http://unitsofmeasure.org' + system: "http://unitsofmeasure.org" code: ml performer: - actor: type: Organization identifier: - system: 'https://fhir.nhs.uk/Id/ods-organization-code' + system: "https://fhir.nhs.uk/Id/ods-organization-code" value: B0C4P display: UNIVERSITY HOSPITAL OF WALES reasonCode: - coding: - - system: 'http://snomed.info/sct' - code: '443684005' + - system: "http://snomed.info/sct" + code: "443684005" display: Disease outbreak (event) protocolApplied: - targetDisease: - coding: - - system: 'http://snomed.info/sct' - code: '55735004' + - system: "http://snomed.info/sct" + code: "55735004" display: >- Respiratory syncytial virus infection (disorder) doseNumberPositiveInt: 1 search: mode: match - - fullUrl: 'urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac' + - fullUrl: "urn:uuid:a7a5bc28-5831-4158-8a73-0d3e6e43c1ac" resource: resourceType: Patient - id: '9449306206' + id: "9449306206" identifier: - - system: 'https://fhir.nhs.uk/Id/nhs-number' - value: '9449306206' - birthDate: '2014-03-25' + - system: "https://fhir.nhs.uk/Id/nhs-number" + value: "9449306206" + birthDate: "2014-03-25" search: mode: include total: 1 @@ -1351,7 +1347,7 @@ components: type: object properties: text: - $ref: '#/components/schemas/Narrative' + $ref: "#/components/schemas/Narrative" description: A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it "clinically safe" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety. contained: type: array @@ -1363,13 +1359,13 @@ components: properties: resourceType: type: string - example : OperationOutcome + example: OperationOutcome id: type: string pattern: '[A-Za-z0-9\-\.]{1,64}' description: The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes. meta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. issue: type: array @@ -1399,20 +1395,20 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. modifierExtension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: |- May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions. - + Modifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself). issue: type: array items: - $ref: '#/components/schemas/OperationOutcome_Issue' + $ref: "#/components/schemas/OperationOutcome_Issue" description: An error, warning, or information message that results from a system action. minItems: 1 required: @@ -1461,13 +1457,13 @@ components: - Uidx_swV4Z Bundle_Entry: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: link: type: array items: - $ref: '#/components/schemas/Bundle_Link' + $ref: "#/components/schemas/Bundle_Link" description: A series of links that provide context to this entry. fullUrl: type: string @@ -1483,7 +1479,7 @@ components: pattern: '[A-Za-z0-9\-\.]{1,64}' description: The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes. meta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. implicitRules: type: string @@ -1494,17 +1490,17 @@ components: pattern: '[^\s]+(\s[^\s]+)*' description: The base language in which the resource is written. search: - $ref: '#/components/schemas/Bundle_Entry_Search' + $ref: "#/components/schemas/Bundle_Entry_Search" description: Information about the search process that lead to the creation of this entry. request: - $ref: '#/components/schemas/Bundle_Entry_Request' + $ref: "#/components/schemas/Bundle_Entry_Request" description: Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry. response: - $ref: '#/components/schemas/Bundle_Entry_Response' + $ref: "#/components/schemas/Bundle_Entry_Response" description: Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history. Bundle_Entry_Response: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: status: @@ -1524,13 +1520,13 @@ components: pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)) description: The date/time that the resource was modified on the server. outcome: - $ref: '#/components/schemas/Resource' + $ref: "#/components/schemas/Resource" description: An OperationOutcome containing hints and warnings produced as part of processing this entry in a batch or transaction. required: - status Bundle_Entry_Request: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: method: @@ -1562,7 +1558,7 @@ components: - url Bundle_Entry_Search: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: mode: @@ -1589,7 +1585,7 @@ components: - url Immunization_ProtocolApplied: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: series: @@ -1597,12 +1593,12 @@ components: pattern: '[ \r\n\t\S]+' description: One possible path to achieve presumed immunity against a disease - within the context of an authority. authority: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Indicates the authority who published the protocol (e.g. ACIP) that is being followed. targetDisease: type: array items: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: The vaccine preventable disease the dose is being administered against. doseNumberPositiveInt: type: integer @@ -1622,7 +1618,7 @@ components: description: The recommended number of doses to achieve immunity. Immunization_Reaction: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: date: @@ -1630,14 +1626,14 @@ components: pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)? description: Date of reaction to the immunization. detail: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Details of the reaction. reported: type: boolean description: Self-reported indicator. Immunization_Education: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: documentType: @@ -1658,20 +1654,20 @@ components: description: Date the educational material was given to the patient. Immunization_Performer: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: function: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Describes the type of performance (e.g. ordering provider, administering provider, etc.). actor: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: The practitioner or organization who performed the action. required: - actor OperationOutcome_Issue: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: severity: @@ -1683,7 +1679,7 @@ components: pattern: '[^\s]+(\s[^\s]+)*' description: Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element. details: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Additional details about the error. This may be a text description of the error or a system code that identifies the error. diagnostics: type: string @@ -1714,7 +1710,7 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. example: - url: http://example.com @@ -1729,19 +1725,19 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. modifierExtension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: |- May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions. - + Modifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself). Address: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: use: @@ -1783,20 +1779,20 @@ components: pattern: '[ \r\n\t\S]+' description: Country - a nation as commonly understood or generally accepted. period: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Time period when address was/is in use. Age: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} Annotation: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: authorReference: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: The individual responsible for making the annotation. authorString: type: string @@ -1814,7 +1810,7 @@ components: - text Attachment: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: contentType: @@ -1859,12 +1855,12 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. coding: type: array items: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: A reference to a code defined by a terminology system. text: type: string @@ -1880,7 +1876,7 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. system: type: string @@ -1903,7 +1899,7 @@ components: description: Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays). ContactPoint: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: system: @@ -1923,26 +1919,26 @@ components: format: int32 description: Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values. period: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Time period when the contact point was/is in use. Count: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} Distance: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} Duration: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} HumanName: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: use: @@ -1976,7 +1972,7 @@ components: pattern: '[ \r\n\t\S]+' description: Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name. period: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Indicates the period of time when this name was valid for the named person. Identifier: type: object @@ -1988,14 +1984,14 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. use: type: string pattern: '[^\s]+(\s[^\s]+)*' description: The purpose of this identifier. type: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: A coded type for the identifier that can be used to determine which identifier to use for a specific purpose. system: type: string @@ -2006,10 +2002,10 @@ components: pattern: '[ \r\n\t\S]+' description: The portion of the identifier typically relevant to the user and which is unique within the context of the system. period: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Time period during which identifier is/was valid for use. assigner: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Organization that issued/manages the identifier. example: reference: Organization/123 @@ -2017,7 +2013,7 @@ components: display: The Assigning Organization Money: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: value: @@ -2029,12 +2025,12 @@ components: description: ISO 4217 Currency Code. MoneyQuantity: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} Period: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: start: @@ -2047,7 +2043,7 @@ components: description: The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time. Quantity: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: value: @@ -2071,25 +2067,25 @@ components: description: A computer processable form of the unit in some unit representation system. Range: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: low: - $ref: '#/components/schemas/SimpleQuantity' + $ref: "#/components/schemas/SimpleQuantity" description: The low limit. The boundary is inclusive. high: - $ref: '#/components/schemas/SimpleQuantity' + $ref: "#/components/schemas/SimpleQuantity" description: The high limit. The boundary is inclusive. Ratio: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: numerator: - $ref: '#/components/schemas/Quantity' + $ref: "#/components/schemas/Quantity" description: The value of the numerator. denominator: - $ref: '#/components/schemas/Quantity' + $ref: "#/components/schemas/Quantity" description: The value of the denominator. Reference: type: object @@ -2101,7 +2097,7 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. reference: type: string @@ -2112,10 +2108,10 @@ components: pattern: \S* description: |- The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent. - + The type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. "Patient" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources). identifier: - $ref: '#/components/schemas/Identifier' + $ref: "#/components/schemas/Identifier" description: An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference. display: type: string @@ -2123,11 +2119,11 @@ components: description: Plain text narrative that identifies the resource in addition to the resource reference. SampledData: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: origin: - $ref: '#/components/schemas/SimpleQuantity' + $ref: "#/components/schemas/SimpleQuantity" description: The base quantity that a measured value of zero represents. In addition, this provides the units of the entire measurement series. period: type: number @@ -2155,18 +2151,18 @@ components: - dimensions SimpleQuantity: allOf: - - $ref: '#/components/schemas/Quantity' + - $ref: "#/components/schemas/Quantity" - type: object - properties: { } + properties: {} Signature: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: type: type: array items: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: An indication of the reason that the entity signed this document. This may be explicitly included as part of the signature information and can be used when determining accountability for various actions concerning the document. minItems: 1 when: @@ -2174,10 +2170,10 @@ components: pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)) description: When the digital signature was signed. who: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: A reference to an application-usable description of the identity that signed (e.g. the signature used their private key). onBehalfOf: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: A reference to an application-usable description of the identity that is represented by the signature. targetFormat: type: string @@ -2197,7 +2193,7 @@ components: - who Timing: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: event: @@ -2207,24 +2203,24 @@ components: pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)? description: Identifies specific times when the event occurs. repeat: - $ref: '#/components/schemas/Timing_Repeat' + $ref: "#/components/schemas/Timing_Repeat" description: A set of rules that describe when the event is scheduled. code: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: A code for the timing schedule (or just text in code.text). Some codes such as BID are ubiquitous, but many institutions define their own additional codes. If a code is provided, the code is understood to be a complete statement of whatever is specified in the structured timing data, and either the code or the data may be used to interpret the Timing, with the exception that .repeat.bounds still applies over the code (and is not contained in the code). Timing_Repeat: allOf: - - $ref: '#/components/schemas/BackboneElement' + - $ref: "#/components/schemas/BackboneElement" - type: object properties: boundsDuration: - $ref: '#/components/schemas/Duration' + $ref: "#/components/schemas/Duration" description: Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule. boundsRange: - $ref: '#/components/schemas/Range' + $ref: "#/components/schemas/Range" description: Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule. boundsPeriod: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Either a duration for the length of the timing schedule, a range of possible length, or outer bounds for start and/or end limits of the timing schedule. count: type: integer @@ -2286,7 +2282,7 @@ components: description: The number of minutes from the event. If the event code does not indicate whether the minutes is before or after the event, then the offset is assumed to be after the event. ContactDetail: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: name: @@ -2296,11 +2292,11 @@ components: telecom: type: array items: - $ref: '#/components/schemas/ContactPoint' + $ref: "#/components/schemas/ContactPoint" description: The contact details for the individual (if a name was provided) or the organization. RelatedArtifact: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: type: @@ -2324,7 +2320,7 @@ components: pattern: \S* description: A url for the artifact that can be followed to access the actual content. document: - $ref: '#/components/schemas/Attachment' + $ref: "#/components/schemas/Attachment" description: The document being referenced, represented as an attachment. This is exclusive with the resource element. resource: type: string @@ -2334,29 +2330,29 @@ components: - type UsageContext: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: code: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: A code that identifies the type of context being specified by this usage context. valueCodeableConcept: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: A value that defines the context specified in this context of use. The interpretation of the value is defined by the code. valueQuantity: - $ref: '#/components/schemas/Quantity' + $ref: "#/components/schemas/Quantity" description: A value that defines the context specified in this context of use. The interpretation of the value is defined by the code. valueRange: - $ref: '#/components/schemas/Range' + $ref: "#/components/schemas/Range" description: A value that defines the context specified in this context of use. The interpretation of the value is defined by the code. valueReference: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: A value that defines the context specified in this context of use. The interpretation of the value is defined by the code. required: - code Meta: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: versionId: @@ -2380,16 +2376,16 @@ components: security: type: array items: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: Security labels applied to this resource. These tags connect specific resources to the overall security policy and infrastructure. tag: type: array items: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: Tags applied to this resource. Tags are intended to be used to identify and relate resources to process and workflow, and applications are not required to consider the tags when interpreting the meaning of a resource. Narrative: allOf: - - $ref: '#/components/schemas/Element' + - $ref: "#/components/schemas/Element" - type: object properties: status: @@ -2412,7 +2408,7 @@ components: extension: type: array items: - $ref: '#/components/schemas/Extension' + $ref: "#/components/schemas/Extension" description: May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. url: type: string @@ -2493,79 +2489,79 @@ components: pattern: urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueAddress: - $ref: '#/components/schemas/Address' + $ref: "#/components/schemas/Address" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueAge: - $ref: '#/components/schemas/Age' + $ref: "#/components/schemas/Age" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueAnnotation: - $ref: '#/components/schemas/Annotation' + $ref: "#/components/schemas/Annotation" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueAttachment: - $ref: '#/components/schemas/Attachment' + $ref: "#/components/schemas/Attachment" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueCodeableConcept: - $ref: '#/components/schemas/CodeableConcept' + $ref: "#/components/schemas/CodeableConcept" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueCoding: - $ref: '#/components/schemas/Coding' + $ref: "#/components/schemas/Coding" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueContactPoint: - $ref: '#/components/schemas/ContactPoint' + $ref: "#/components/schemas/ContactPoint" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueCount: - $ref: '#/components/schemas/Count' + $ref: "#/components/schemas/Count" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueDistance: - $ref: '#/components/schemas/Distance' + $ref: "#/components/schemas/Distance" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueDuration: - $ref: '#/components/schemas/Duration' + $ref: "#/components/schemas/Duration" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueHumanName: - $ref: '#/components/schemas/HumanName' + $ref: "#/components/schemas/HumanName" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueIdentifier: - $ref: '#/components/schemas/Identifier' + $ref: "#/components/schemas/Identifier" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueMoney: - $ref: '#/components/schemas/Money' + $ref: "#/components/schemas/Money" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valuePeriod: - $ref: '#/components/schemas/Period' + $ref: "#/components/schemas/Period" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueQuantity: - $ref: '#/components/schemas/Quantity' + $ref: "#/components/schemas/Quantity" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueRange: - $ref: '#/components/schemas/Range' + $ref: "#/components/schemas/Range" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueRatio: - $ref: '#/components/schemas/Ratio' + $ref: "#/components/schemas/Ratio" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueReference: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueSampledData: - $ref: '#/components/schemas/SampledData' + $ref: "#/components/schemas/SampledData" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueSignature: - $ref: '#/components/schemas/Signature' + $ref: "#/components/schemas/Signature" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueTiming: - $ref: '#/components/schemas/Timing' + $ref: "#/components/schemas/Timing" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueContactDetail: - $ref: '#/components/schemas/ContactDetail' + $ref: "#/components/schemas/ContactDetail" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueRelatedArtifact: - $ref: '#/components/schemas/RelatedArtifact' + $ref: "#/components/schemas/RelatedArtifact" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueUsageContext: - $ref: '#/components/schemas/UsageContext' + $ref: "#/components/schemas/UsageContext" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). valueMeta: - $ref: '#/components/schemas/Meta' + $ref: "#/components/schemas/Meta" description: Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list). required: - - url \ No newline at end of file + - url diff --git a/temporary_sandbox/fhir_api/__init__.py b/temporary_sandbox/fhir_api/__init__.py index 34d5ce84d..875867b54 100644 --- a/temporary_sandbox/fhir_api/__init__.py +++ b/temporary_sandbox/fhir_api/__init__.py @@ -1,6 +1,7 @@ -''' +""" APP for fastapi -''' +""" + import os from fastapi import FastAPI @@ -16,13 +17,13 @@ app = FastAPI( - title=os.getenv('FASTAPI_TITLE', 'Immunisation Fhir API'), - description=os.getenv( - 'FASTAPI_DESC', 'API'), - version=os.getenv('VERSION', 'DEVELOPMENT'), + title=os.getenv("FASTAPI_TITLE", "Immunisation Fhir API"), + description=os.getenv("FASTAPI_DESC", "API"), + version=os.getenv("VERSION", "DEVELOPMENT"), root_path=f'/{os.getenv("SERVICE_BASE_PATH")}/', docs_url="/documentation", - redoc_url="/redocumentation") + redoc_url="/redocumentation", +) # ENDPOINT ROUTERS diff --git a/temporary_sandbox/fhir_api/exceptions/base_exceptions.py b/temporary_sandbox/fhir_api/exceptions/base_exceptions.py index 7f2701b18..b2cf0a4f0 100644 --- a/temporary_sandbox/fhir_api/exceptions/base_exceptions.py +++ b/temporary_sandbox/fhir_api/exceptions/base_exceptions.py @@ -1,5 +1,4 @@ -""" Base Exceptions that can be used across all collections """ - +"""Base Exceptions that can be used across all collections""" # pylint: disable=W0231 @@ -11,12 +10,13 @@ AlreadyExistsError, WebSocketError, BaseError, - BaseIdentifiedError + BaseIdentifiedError, ) class BaseAPIException(Exception): - """ Base Error for custom API exceptions """ + """Base Error for custom API exceptions""" + message = "Exception Occured" code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR model = BaseError @@ -27,10 +27,7 @@ def __init__(self, **kwargs): self.data = self.model(**kwargs) def response(self): - return JSONResponse( - content=self.data.dict(), - status_code=self.code - ) + return JSONResponse(content=self.data.dict(), status_code=self.code) @classmethod def response_model(cls): @@ -39,6 +36,7 @@ def response_model(cls): class BaseIdentifiedException(BaseAPIException): """Base error for exceptions related with entities, uniquely identified""" + message = "Entity error" code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR model = BaseIdentifiedError @@ -49,6 +47,7 @@ def __init__(self, identifier, **kwargs): class NotFoundException(BaseIdentifiedException): """Base error for exceptions raised because an entity does not exist""" + message = "The entity does not exist" code = fastapi.status.HTTP_404_NOT_FOUND model = NotFoundError @@ -56,20 +55,22 @@ class NotFoundException(BaseIdentifiedException): class AlreadyExistsException(BaseIdentifiedException): """Base error for exceptions raised because an entity already exists""" + message = "The entity already exists" code = fastapi.status.HTTP_409_CONFLICT model = AlreadyExistsError class WebSocketException(BaseAPIException): - """ Base Error for websocket exceptions """ + """Base Error for websocket exceptions""" + message = "Websocket encountered an error" code = fastapi.status.HTTP_418_IM_A_TEAPOT model = WebSocketError def get_exception_responses(*args: Type[BaseAPIException]) -> dict: - """ return a dict of responses used on FastAPI endpoint """ + """return a dict of responses used on FastAPI endpoint""" responses = {} for cls in args: responses.update(cls.response_model()) diff --git a/temporary_sandbox/fhir_api/models/dynamodb/data_input.py b/temporary_sandbox/fhir_api/models/dynamodb/data_input.py index 409b7a356..a926ab875 100644 --- a/temporary_sandbox/fhir_api/models/dynamodb/data_input.py +++ b/temporary_sandbox/fhir_api/models/dynamodb/data_input.py @@ -1,4 +1,4 @@ -''' Data input model for DynamoDB ''' +"""Data input model for DynamoDB""" from typing import Optional from pydantic import ( @@ -9,11 +9,13 @@ class DataInput(BaseModel): - ''' Data input model ''' + """Data input model""" + nhsNumber: str = FhirR4Fields.string data: Optional[dict] class SuccessModel(BaseModel): - ''' SuccessModel for interacting with DynamoDB''' + """SuccessModel for interacting with DynamoDB""" + success: bool = FhirR4Fields.boolean diff --git a/temporary_sandbox/fhir_api/models/dynamodb/read_models.py b/temporary_sandbox/fhir_api/models/dynamodb/read_models.py index 08868d838..b34c3557d 100644 --- a/temporary_sandbox/fhir_api/models/dynamodb/read_models.py +++ b/temporary_sandbox/fhir_api/models/dynamodb/read_models.py @@ -1,4 +1,5 @@ -''' Read Models for Dynamodb''' +"""Read Models for Dynamodb""" + from typing import ( Union, Literal, @@ -15,14 +16,16 @@ class Resource(BaseModel): - ''' Wrapper Model for returned resource ''' + """Wrapper Model for returned resource""" + fullUrl: str = FhirR4Fields.uri resource: Union[Immunization, Patient] search: Optional[dict] = {"mode": "match"} # This looks to be api specific class BatchImmunizationRead(BaseModel): - ''' Model for Multiple records ''' + """Model for Multiple records""" + resourceType: Literal["Bundle"] = Field(default="Bundle") type: str = FhirR4Fields.string total: int = FhirR4Fields.integer diff --git a/temporary_sandbox/fhir_api/models/dynamodb/table.py b/temporary_sandbox/fhir_api/models/dynamodb/table.py index 3c685df49..d12cbf250 100644 --- a/temporary_sandbox/fhir_api/models/dynamodb/table.py +++ b/temporary_sandbox/fhir_api/models/dynamodb/table.py @@ -1,4 +1,4 @@ -''' Models for interacting with tables ''' +"""Models for interacting with tables""" from pydantic import ( BaseModel, diff --git a/temporary_sandbox/fhir_api/models/dynamodb/update_model.py b/temporary_sandbox/fhir_api/models/dynamodb/update_model.py index df9cf66cb..33cb38266 100644 --- a/temporary_sandbox/fhir_api/models/dynamodb/update_model.py +++ b/temporary_sandbox/fhir_api/models/dynamodb/update_model.py @@ -1,4 +1,4 @@ -''' Update Model for Immunization Records ''' +"""Update Model for Immunization Records""" import datetime @@ -13,16 +13,13 @@ Quantity, ) -from fhir_api.models.fhir_r4.immunization import ( - Performer, - ProtocolApplied, - Annotation -) +from fhir_api.models.fhir_r4.immunization import Performer, ProtocolApplied, Annotation class UpdateImmunizationRecord(BaseModel): - '''Update Immunization Record ''' - resourceType: Literal["Immunization"] = Field(default='Immunization') + """Update Immunization Record""" + + resourceType: Literal["Immunization"] = Field(default="Immunization") status: Optional[code_types.status_codes] statusReason: Optional[CodeableConceptType] vaccineCode: Optional[CodeableConceptType] @@ -50,7 +47,7 @@ class UpdateImmunizationRecord(BaseModel): def dict(self, *args, **kwargs) -> dict[str, Any]: """ - Override the default dict method to exclude None values in the response + Override the default dict method to exclude None values in the response """ - kwargs.pop('exclude_none', None) + kwargs.pop("exclude_none", None) return super().dict(*args, exclude_none=True, **kwargs) diff --git a/temporary_sandbox/fhir_api/models/fhir_r4/code_types.py b/temporary_sandbox/fhir_api/models/fhir_r4/code_types.py index e56273aec..e430d2be0 100644 --- a/temporary_sandbox/fhir_api/models/fhir_r4/code_types.py +++ b/temporary_sandbox/fhir_api/models/fhir_r4/code_types.py @@ -1,9 +1,9 @@ -''' Literal Code types used by data models''' +"""Literal Code types used by data models""" from typing import Literal contact_point_system_types = Literal["phone", "fax", "email", "pager", "url", "sms", "other"] -contact_point_use_types = Literal['home', 'work', 'temp', 'old', 'mobile'] +contact_point_use_types = Literal["home", "work", "temp", "old", "mobile"] address_use_type = Literal["home", "work", "temp", "old", "billing"] address_type_type = Literal["postal", "physical", "both"] diff --git a/temporary_sandbox/fhir_api/models/fhir_r4/common.py b/temporary_sandbox/fhir_api/models/fhir_r4/common.py index 5c09c03d0..d303a4589 100644 --- a/temporary_sandbox/fhir_api/models/fhir_r4/common.py +++ b/temporary_sandbox/fhir_api/models/fhir_r4/common.py @@ -1,14 +1,10 @@ -''' Common FHIR Data Models ''' +"""Common FHIR Data Models""" from typing import ( Optional, ) -from pydantic import ( - BaseModel, - validator, - PositiveInt - ) +from pydantic import BaseModel, validator, PositiveInt from datetime import datetime @@ -17,7 +13,8 @@ class CodingType(BaseModel): - ''' Code Data Model ''' + """Code Data Model""" + system: Optional[str] = FhirR4Fields.string version: Optional[str] = FhirR4Fields.string code: Optional[str] = FhirR4Fields.string @@ -26,21 +23,24 @@ class CodingType(BaseModel): class CodeableConceptType(BaseModel): - ''' Codeable Concept Data Model ''' + """Codeable Concept Data Model""" + coding: Optional[list[CodingType]] text: Optional[str] = FhirR4Fields.string class Period(BaseModel): - ''' A time period defined by a start and end date/time. ''' + """A time period defined by a start and end date/time.""" + start: datetime = FhirR4Fields.dateTime end: datetime = FhirR4Fields.dateTime class HumanName(BaseModel): - ''' + """ A name of a human with text, parts and usage information. - ''' + """ + use: Optional[code_types.human_name_use] text: Optional[str] = FhirR4Fields.string family: Optional[str] = FhirR4Fields.string @@ -51,7 +51,8 @@ class HumanName(BaseModel): class Quantity(BaseModel): - ''' Quantity Type ''' + """Quantity Type""" + value: Optional[float] = FhirR4Fields.decimal comparator: Optional[str] = FhirR4Fields.code unit: Optional[str] = FhirR4Fields.string @@ -60,31 +61,33 @@ class Quantity(BaseModel): class ContactPoint(BaseModel): - ''' + """ Details for all kinds of technology-mediated contact points for a person or organization, including telephone, email, etc. - ''' + """ + system: code_types.contact_point_system_types = None # Required value: Optional[str] = FhirR4Fields.string use: Optional[code_types.contact_point_use_types] rank: Optional[PositiveInt] = FhirR4Fields.positiveInt period: Optional[Period] - @validator('system') + @validator("system") def value_validator(cls, _v): - ''' cpt-2: A system is required is a value is provided ''' + """cpt-2: A system is required is a value is provided""" if cls.value: assert _v is None, "System must be populated if a value exists" return _v class Address(BaseModel): - ''' + """ An address expressed using postal conventions (as opposed to GPS or other location definition formats). This data type may be used to convey addresses for use in delivering mail as well as for visiting locations which might not be valid for mail delivery. There are a variety of postal address formats defined around the world. - ''' + """ + use: Optional[code_types.address_use_type] type: Optional[code_types.address_type_type] text: Optional[str] = FhirR4Fields.string @@ -98,7 +101,8 @@ class Address(BaseModel): class Reference(BaseModel): - ''' Reference data Model ''' + """Reference data Model""" + reference: Optional[str] = FhirR4Fields.string type: Optional[str] = FhirR4Fields.string identifier: Optional["Identifier"] @@ -106,13 +110,15 @@ class Reference(BaseModel): class CodeableReference(BaseModel): - ''' Codeable Reference ''' + """Codeable Reference""" + concept: Optional[CodeableConceptType] reference: Optional[Reference] class Identifier(BaseModel): - ''' Identifier Data Model ''' + """Identifier Data Model""" + use_type: Optional[str] = FhirR4Fields.string type: Optional[CodeableConceptType] system: Optional[str] = FhirR4Fields.string @@ -122,7 +128,8 @@ class Identifier(BaseModel): class Attachment(BaseModel): - ''' Attachment Model ''' + """Attachment Model""" + contentType: Optional[str] = FhirR4Fields.string language: Optional[str] = FhirR4Fields.string data: Optional[str] = FhirR4Fields.base64Binary diff --git a/temporary_sandbox/fhir_api/models/fhir_r4/fhir_datatype_fields.py b/temporary_sandbox/fhir_api/models/fhir_r4/fhir_datatype_fields.py index 93de5ce31..e3a276ae2 100644 --- a/temporary_sandbox/fhir_api/models/fhir_r4/fhir_datatype_fields.py +++ b/temporary_sandbox/fhir_api/models/fhir_r4/fhir_datatype_fields.py @@ -1,4 +1,4 @@ -''' Generic Fields for FHIR Revision 4 ''' +"""Generic Fields for FHIR Revision 4""" import datetime @@ -13,11 +13,9 @@ class FhirR4Fields: - """ Generic Fields, descriptions and examples""" - _pk: str = Field( - description="Partition Key for DynamoDB", - example="P#1000" - ) + """Generic Fields, descriptions and examples""" + + _pk: str = Field(description="Partition Key for DynamoDB", example="P#1000") _sk: str = Field( description="Sorting Key for DynamoDB", @@ -49,7 +47,7 @@ class FhirR4Fields: uri: str = Field( description="A Uniform Resource Identifier Reference", example="urn:uuid:53fefa32-fcbb-4ff8-8a92-55ee120877b7", - regex=r"\S*" + regex=r"\S*", ) url: AnyUrl = Field( @@ -66,7 +64,7 @@ class FhirR4Fields: base64Binary: str = Field( description="A stream of bytes, base64 encoded", example="eyJmbGF2b3IiOiAiZnJlbmNoIHZhbmlsbGEiLCAic2l6ZSI6ICIyNCBveiJ9", - regex=r"(\s*([0-9a-zA-Z\+\=]){4}\s*)+" + regex=r"(\s*([0-9a-zA-Z\+\=]){4}\s*)+", ) instant: datetime.datetime = Field( @@ -77,10 +75,10 @@ class FhirR4Fields: date: Union[ datetime.date, partial(datetime.date, day=1), - partial(datetime.date, month=1, day=1) + partial(datetime.date, month=1, day=1), ] = Field( description="A date, or partial date (e.g. just year or year + month) as used in human communication", - example="1905-08-23" + example="1905-08-23", ) dateTime: str = Field( @@ -90,26 +88,26 @@ class FhirR4Fields: time: datetime.time = Field( description="A time during the day, in the format hh:mm:ss. There is no date specified.", - example="01:00:01" + example="01:00:01", ) code: str = Field( description="Indicates that the value is taken from a set of controlled strings defined elsewhere", example="home", - regex=r"[^\s]+(\s[^\s]+)*" + regex=r"[^\s]+(\s[^\s]+)*", ) oid: str = Field( description="An OID represented as a URI", example="urn:oid:1.2.3.4.5", - regex=r"urn:oid:[0-2](\.(0|[1-9][0-9]*))+" + regex=r"urn:oid:[0-2](\.(0|[1-9][0-9]*))+", ) id: str = Field( description="Any combination of upper- or lower-case ASCII letters", example="53fefa32-fcbb-4ff8-8a92-55ee120877b7", regex=r"[A-Za-z0-9\-\.]{1,64}", - max_length=64 + max_length=64, ) markdown: str = Field( @@ -129,5 +127,5 @@ class FhirR4Fields: uuid: str = Field( description="A UUID (aka GUID) represented as a URI", - example="urn:uuid:c757873d-ec9a-4326-a141-556f43239520" + example="urn:uuid:c757873d-ec9a-4326-a141-556f43239520", ) diff --git a/temporary_sandbox/fhir_api/models/fhir_r4/immunization.py b/temporary_sandbox/fhir_api/models/fhir_r4/immunization.py index ca096b814..da4b53fb4 100644 --- a/temporary_sandbox/fhir_api/models/fhir_r4/immunization.py +++ b/temporary_sandbox/fhir_api/models/fhir_r4/immunization.py @@ -1,10 +1,7 @@ -''' Immunization Data Model based on Fhir Revision 4 spec ''' +"""Immunization Data Model based on Fhir Revision 4 spec""" from typing import Optional, Literal, Any -from pydantic import ( - BaseModel, - PositiveInt -) +from pydantic import BaseModel, PositiveInt import datetime import fhir_api.models.fhir_r4.code_types as code_types @@ -33,20 +30,23 @@ class ProtocolApplied(BaseModel): class Author(BaseModel): - ''' Author Model ''' + """Author Model""" + authorRefernce: Optional[Reference] authorString: Optional[str] = FhirR4Fields.string class Annotation(BaseModel): - ''' Annotation Model ''' + """Annotation Model""" + author: Optional[Author] time: Optional[datetime.datetime] = FhirR4Fields.dateTime text: Optional[str] = FhirR4Fields.markdown class Immunization(BaseModel): - ''' Immunization Record for Reading ''' + """Immunization Record for Reading""" + resourceType: Literal["Immunization"] identifier: Optional[list[Identifier]] status: code_types.status_codes @@ -77,7 +77,7 @@ class Immunization(BaseModel): def dict(self, *args, **kwargs) -> dict[str, Any]: """ - Override the default dict method to exclude None values in the response + Override the default dict method to exclude None values in the response """ - kwargs.pop('exclude_none', None) + kwargs.pop("exclude_none", None) return super().dict(*args, exclude_none=True, **kwargs) diff --git a/temporary_sandbox/fhir_api/models/fhir_r4/patient.py b/temporary_sandbox/fhir_api/models/fhir_r4/patient.py index 6eaa6cd9c..d77d09d51 100644 --- a/temporary_sandbox/fhir_api/models/fhir_r4/patient.py +++ b/temporary_sandbox/fhir_api/models/fhir_r4/patient.py @@ -1,4 +1,4 @@ -''' Patient Data Model based on Fhir Revision 4 spec ''' +"""Patient Data Model based on Fhir Revision 4 spec""" from datetime import datetime from typing import Optional, Literal @@ -21,7 +21,8 @@ class Contact(BaseModel): - ''' Contact Model ''' + """Contact Model""" + relationship: Optional[list[CodeableConceptType]] name: Optional[HumanName] telecom: Optional[list[ContactPoint]] @@ -32,19 +33,22 @@ class Contact(BaseModel): class Communication(BaseModel): - ''' Communication Model ''' + """Communication Model""" + language: CodeableConceptType preferred: Optional[bool] = FhirR4Fields.boolean class Link(BaseModel): - ''' Link Model ''' + """Link Model""" + other: Reference type: code_types.link_code class Patient(BaseModel): - ''' Patient Base Model ''' + """Patient Base Model""" + resourceType: Literal["Patient"] identifier: Optional[list[Identifier]] active: bool = FhirR4Fields.boolean diff --git a/temporary_sandbox/fhir_api/routes/dynamodb.py b/temporary_sandbox/fhir_api/routes/dynamodb.py index d61008f61..1548fe0eb 100644 --- a/temporary_sandbox/fhir_api/routes/dynamodb.py +++ b/temporary_sandbox/fhir_api/routes/dynamodb.py @@ -1,4 +1,4 @@ -''' DynamoDB Router Methods ''' +"""DynamoDB Router Methods""" import json @@ -7,22 +7,22 @@ from fhir_api.models.dynamodb.read_models import BatchImmunizationRead -ENDPOINT = '/Immunization' +ENDPOINT = "/Immunization" router = APIRouter(prefix=ENDPOINT) @router.get( "", description="Read Method for Immunization Endpoint", - tags=['Dynamodb', 'CRUD', 'Read'], - response_model=BatchImmunizationRead + tags=["Dynamodb", "CRUD", "Read"], + response_model=BatchImmunizationRead, ) def read_immunization_record( nhsNumber: str, fullUrl: Optional[str] = None, from_date: Optional[str] = None, to_date: Optional[str] = "9999-01-01", - include_record: Optional[str] = None + include_record: Optional[str] = None, ) -> BatchImmunizationRead: with open("/sandbox/fhir_api/sandbox_data.json", "r") as input: diff --git a/temporary_sandbox/fhir_api/routes/root.py b/temporary_sandbox/fhir_api/routes/root.py index adef3b71d..f900ec312 100644 --- a/temporary_sandbox/fhir_api/routes/root.py +++ b/temporary_sandbox/fhir_api/routes/root.py @@ -7,8 +7,6 @@ router = APIRouter() -@router.get('/') +@router.get("/") def root(): - return PlainTextResponse( - os.getenv('FASTAPI_TITLE', 'FHIR API') - ) + return PlainTextResponse(os.getenv("FASTAPI_TITLE", "FHIR API")) diff --git a/temporary_sandbox/fhir_api/routes/status_endpoints.py b/temporary_sandbox/fhir_api/routes/status_endpoints.py index 153a82a16..78e074b5f 100644 --- a/temporary_sandbox/fhir_api/routes/status_endpoints.py +++ b/temporary_sandbox/fhir_api/routes/status_endpoints.py @@ -1,4 +1,4 @@ -""" Sandbox endpoints """ +"""Sandbox endpoints""" from fastapi import APIRouter from fastapi.responses import PlainTextResponse @@ -6,23 +6,24 @@ router = APIRouter() -@router.get('/_ping', - description="Ping check endpoint only returns 200") +@router.get("/_ping", description="Ping check endpoint only returns 200") def ping(): - """ ping sandbox """ + """ping sandbox""" return PlainTextResponse(None, 200) -@router.get('/_status', - description="Status check endpoint only returns 200") +@router.get("/_status", description="Status check endpoint only returns 200") def status(): - """ status sandbox """ - data = {"status": "pass", "ping": "pong", "service": "immunisations-fhir-api", "version": "{}"} + """status sandbox""" + data = { + "status": "pass", + "ping": "pong", + "service": "immunisations-fhir-api", + "version": "{}", + } return data -@router.get('/health', - description="Health check endpoint only returns 200", - tags=['health']) +@router.get("/health", description="Health check endpoint only returns 200", tags=["health"]) def health(): return PlainTextResponse(None, 200) diff --git a/temporary_sandbox/fhir_api/sandbox_data.json b/temporary_sandbox/fhir_api/sandbox_data.json index 16a036050..8772ec225 100644 --- a/temporary_sandbox/fhir_api/sandbox_data.json +++ b/temporary_sandbox/fhir_api/sandbox_data.json @@ -1,116 +1,116 @@ { - "resourceType": "Bundle", - "type": "targeted", - "total": 1, - "entry": [ - { - "fullUrl": "urn:uuid:e36a489e-7981-46c8-8e06-f442cfdf0578", - "resource": { - "resourceType": "Immunization", - "identifier": [ + "resourceType": "Bundle", + "type": "targeted", + "total": 1, + "entry": [ + { + "fullUrl": "urn:uuid:e36a489e-7981-46c8-8e06-f442cfdf0578", + "resource": { + "resourceType": "Immunization", + "identifier": [ + { + "system": "https://supplierABC/identifiers/vacc", + "value": "7813712" + }, + { + "system": "https://supplierABC/identifiers/vacc", + "value": "4294646" + } + ], + "status": "entered-in-error", + "vaccineCode": { + "coding": [ { - "system": "https://supplierABC/identifiers/vacc", - "value": "7813712" + "system": "http://snomed.info/sct", + "code": "7791476", + "display": "fm blades metabolism strategy sole side minority soft animated cnetcom rights admission rules abroad grey" }, { - "system": "https://supplierABC/identifiers/vacc", - "value": "4294646" + "system": "http://snomed.info/sct", + "code": "8263634", + "display": "tgp yugoslavia cds takes comedy registration ronald further belts symposium activated mhz defined ef bedrooms" } - ], - "status": "entered-in-error", - "vaccineCode": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "7791476", - "display": "fm blades metabolism strategy sole side minority soft animated cnetcom rights admission rules abroad grey" - }, - { - "system": "http://snomed.info/sct", - "code": "8263634", - "display": "tgp yugoslavia cds takes comedy registration ronald further belts symposium activated mhz defined ef bedrooms" - } - ] - }, - "patient": { - "reference": "urn:uuid:02be8166-bde8-4925-88cd-0778dc685570", - "type": "Patient", - "identifier": { - "system": "https://supplierABC/identifiers/vacc", - "value": "4829723" + ] + }, + "patient": { + "reference": "urn:uuid:02be8166-bde8-4925-88cd-0778dc685570", + "type": "Patient", + "identifier": { + "system": "https://supplierABC/identifiers/vacc", + "value": "4829723" + } + }, + "occurrenceDateTime": "2020-09-01T05:50:41.118609", + "recorded": "2022-02-14", + "primarySource": true, + "reportOrigin": {}, + "manufacturer": { + "reference": "urn:uuid:ac18d3ac-816b-4619-a2fb-0eefd7ea0e5d", + "type": "Manufacturer", + "identifier": { + "system": "https://supplierABC/identifiers/vacc", + "value": "1651921" + } + }, + "lotNumber": "TMA0", + "expirationDate": "2021-01-21", + "site": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "4271227", + "display": "wow hub knight treaty seminars totals civil indiana carol selective reviewer confidential incentive saw scanners" } - }, - "occurrenceDateTime": "2020-09-01T05:50:41.118609", - "recorded": "2022-02-14", - "primarySource": true, - "reportOrigin": {}, - "manufacturer": { - "reference": "urn:uuid:ac18d3ac-816b-4619-a2fb-0eefd7ea0e5d", - "type": "Manufacturer", - "identifier": { - "system": "https://supplierABC/identifiers/vacc", - "value": "1651921" + ] + }, + "route": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "5751353", + "display": "tight obituaries directions erik twist layer category taste pulse mechanisms exhibitions lots pitch hawk ever" } - }, - "lotNumber": "TMA0", - "expirationDate": "2021-01-21", - "site": { + ] + }, + "doseQuantity": { + "value": 25, + "unit": "horizontal seek lance dramatic ties cube anna", + "system": "http://snomed.info/sct", + "code": "2855493871015690" + }, + "performer": [ + { + "actor": { + "reference": "urn:uuid:5548ae78-8d34-447a-91e6-24899aa642ce", + "type": "Organization", + "identifier": { + "system": "https://supplierABC/identifiers/vacc", + "value": "1506853" + }, + "display": "assembled-testimonials" + } + } + ], + "reasonCode": [ + { "coding": [ { "system": "http://snomed.info/sct", - "code": "4271227", - "display": "wow hub knight treaty seminars totals civil indiana carol selective reviewer confidential incentive saw scanners" - } - ] - }, - "route": { - "coding": [ + "code": "4280288", + "display": "meters repairs fine bahrain flyer doctors flights oven owen cayman andrews identity vehicles muscle button" + }, { "system": "http://snomed.info/sct", - "code": "5751353", - "display": "tight obituaries directions erik twist layer category taste pulse mechanisms exhibitions lots pitch hawk ever" + "code": "1696672", + "display": "coordinate learners inns z date shipment ah oval writing defence camera actors dis debut jenny" } ] - }, - "doseQuantity": { - "value": 25, - "unit": "horizontal seek lance dramatic ties cube anna", - "system": "http://snomed.info/sct", - "code": "2855493871015690" - }, - "performer": [ - { - "actor": { - "reference": "urn:uuid:5548ae78-8d34-447a-91e6-24899aa642ce", - "type": "Organization", - "identifier": { - "system": "https://supplierABC/identifiers/vacc", - "value": "1506853" - }, - "display": "assembled-testimonials" - } - } - ], - "reasonCode": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "4280288", - "display": "meters repairs fine bahrain flyer doctors flights oven owen cayman andrews identity vehicles muscle button" - }, - { - "system": "http://snomed.info/sct", - "code": "1696672", - "display": "coordinate learners inns z date shipment ah oval writing defence camera actors dis debut jenny" - } - ] - } - ] - }, - "search": { - "mode": "query" - } + } + ] + }, + "search": { + "mode": "query" } - ] - } \ No newline at end of file + } + ] +} diff --git a/terraform/README.md b/terraform/README.md index 9d9d090bb..8f3804925 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,4 +1,5 @@ # About + The Terraform configuration in this folder is executed in each PR and sets up lambdas associated with the PR. Once the PR is merged, it will be used by the release pipeline to deploy to INT and REF. This is also run by the production release pipeline to deploy the lambdas to the prod blue and green sub environments. ## Environments Structure @@ -14,13 +15,16 @@ The environment-specific configuration is structured as follows: The `Makefile` automatically reads the `.env` file to determine the correct `variables.tfvars` file to use, allowing customization of infrastructure for each sub-environment. ## Run locally + 1. Create a `.env` file with the following values: + ```dotenv ENVIRONMENT=dev # Target AWS account (e.g., dev, int, prod) SUB_ENVIRONMENT=pr-123 # Sub-environment (e.g., pr-57, internal-dev) AWS_REGION=eu-west-2 AWS_PROFILE=your-aws-profile ``` + 2. Run `make init` to download providers and dependencies 3. Run `make plan` to output plan with the changes that terraform will perform 4. **WARNING**: Run `make apply` only after thoroughly reviewing the plan as this might destroy or modify existing infrastructure diff --git a/terraform/ack_lambda.tf b/terraform/ack_lambda.tf index 1757512b6..2b12e6e8d 100644 --- a/terraform/ack_lambda.tf +++ b/terraform/ack_lambda.tf @@ -19,11 +19,11 @@ resource "aws_ecr_repository" "ack_lambda_repository" { # Module for building and pushing Docker image to ECR module "ack_processor_docker_image" { - source = "terraform-aws-modules/lambda/aws//modules/docker-build" - version = "8.1.0" + source = "terraform-aws-modules/lambda/aws//modules/docker-build" + version = "8.1.0" docker_file_path = "./ack_backend/Dockerfile" - create_ecr_repo = false - ecr_repo = aws_ecr_repository.ack_lambda_repository.name + create_ecr_repo = false + ecr_repo = aws_ecr_repository.ack_lambda_repository.name ecr_repo_lifecycle_policy = jsonencode({ "rules" : [ { @@ -43,7 +43,7 @@ module "ack_processor_docker_image" { platform = "linux/amd64" use_image_tag = false - source_path = abspath("${path.root}/../lambdas") + source_path = abspath("${path.root}/../lambdas") triggers = { dir_sha = local.ack_lambda_dir_sha shared_dir_sha = local.shared_dir_sha @@ -212,10 +212,10 @@ resource "aws_lambda_function" "ack_processor_lambda" { environment { variables = { - ACK_BUCKET_NAME = aws_s3_bucket.batch_data_destination_bucket.bucket - SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name - SOURCE_BUCKET_NAME = aws_s3_bucket.batch_data_source_bucket.bucket - AUDIT_TABLE_NAME = aws_dynamodb_table.audit-table.name + ACK_BUCKET_NAME = aws_s3_bucket.batch_data_destination_bucket.bucket + SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name + SOURCE_BUCKET_NAME = aws_s3_bucket.batch_data_source_bucket.bucket + AUDIT_TABLE_NAME = aws_dynamodb_table.audit-table.name } } diff --git a/terraform/batch_processor_filter_lambda.tf b/terraform/batch_processor_filter_lambda.tf index bcee7f52f..58be63e9e 100644 --- a/terraform/batch_processor_filter_lambda.tf +++ b/terraform/batch_processor_filter_lambda.tf @@ -303,9 +303,9 @@ resource "aws_lambda_event_source_mapping" "batch_file_created_sqs_to_lambda" { } resource "aws_cloudwatch_log_metric_filter" "batch_processor_filter_error_logs" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 - name = "${local.short_prefix}-BatchProcessorFilterErrorLogsFilter" + name = "${local.short_prefix}-BatchProcessorFilterErrorLogsFilter" # Ignore errors with the below exception type. This is an expected error which returns items to the queue pattern = "\"[ERROR]\" -EventAlreadyProcessingForSupplierAndVaccTypeError" log_group_name = aws_cloudwatch_log_group.batch_processor_filter_lambda_log_group.name @@ -318,7 +318,7 @@ resource "aws_cloudwatch_log_metric_filter" "batch_processor_filter_error_logs" } resource "aws_cloudwatch_metric_alarm" "batch_processor_filter_error_alarm" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 alarm_name = "${local.short_prefix}-batch-processor-filter-lambda-error" comparison_operator = "GreaterThanOrEqualToThreshold" diff --git a/terraform/ecs_batch_processor_config.tf b/terraform/ecs_batch_processor_config.tf index b67372acc..422909c4c 100644 --- a/terraform/ecs_batch_processor_config.tf +++ b/terraform/ecs_batch_processor_config.tf @@ -359,7 +359,7 @@ resource "aws_cloudwatch_log_group" "pipe_log_group" { } resource "aws_cloudwatch_log_metric_filter" "record_processor_task_error_logs" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 name = "${local.short_prefix}-RecordProcessorTaskErrorLogsFilter" pattern = "%ERROR:%" @@ -373,7 +373,7 @@ resource "aws_cloudwatch_log_metric_filter" "record_processor_task_error_logs" { } resource "aws_cloudwatch_metric_alarm" "record_processor_task_error_alarm" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 alarm_name = "${local.short_prefix}-record-processor-task-error" comparison_operator = "GreaterThanOrEqualToThreshold" diff --git a/terraform/endpoints.tf b/terraform/endpoints.tf index 7ab6b06ec..003918ac1 100644 --- a/terraform/endpoints.tf +++ b/terraform/endpoints.tf @@ -108,16 +108,16 @@ output "oas" { module "api_gateway" { source = "./modules/api_gateway" - prefix = local.prefix - short_prefix = local.short_prefix - zone_id = data.aws_route53_zone.project_zone.zone_id - api_domain_name = local.service_domain_name - environment = var.environment - sub_environment = var.sub_environment - oas = local.oas - aws_region = var.aws_region + prefix = local.prefix + short_prefix = local.short_prefix + zone_id = data.aws_route53_zone.project_zone.zone_id + api_domain_name = local.service_domain_name + environment = var.environment + sub_environment = var.sub_environment + oas = local.oas + aws_region = var.aws_region immunisation_account_id = var.immunisation_account_id - csoc_account_id = var.csoc_account_id + csoc_account_id = var.csoc_account_id } resource "aws_lambda_permission" "api_gw" { diff --git a/terraform/file_name_processor.tf b/terraform/file_name_processor.tf index 2bee47988..7c6727123 100644 --- a/terraform/file_name_processor.tf +++ b/terraform/file_name_processor.tf @@ -276,14 +276,14 @@ resource "aws_lambda_function" "file_processor_lambda" { environment { variables = { - SOURCE_BUCKET_NAME = aws_s3_bucket.batch_data_source_bucket.bucket - ACK_BUCKET_NAME = aws_s3_bucket.batch_data_destination_bucket.bucket - QUEUE_URL = aws_sqs_queue.batch_file_created.url - REDIS_HOST = data.aws_elasticache_cluster.existing_redis.cache_nodes[0].address - REDIS_PORT = data.aws_elasticache_cluster.existing_redis.cache_nodes[0].port - SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name - AUDIT_TABLE_NAME = aws_dynamodb_table.audit-table.name - AUDIT_TABLE_TTL_DAYS = 60 + SOURCE_BUCKET_NAME = aws_s3_bucket.batch_data_source_bucket.bucket + ACK_BUCKET_NAME = aws_s3_bucket.batch_data_destination_bucket.bucket + QUEUE_URL = aws_sqs_queue.batch_file_created.url + REDIS_HOST = data.aws_elasticache_cluster.existing_redis.cache_nodes[0].address + REDIS_PORT = data.aws_elasticache_cluster.existing_redis.cache_nodes[0].port + SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name + AUDIT_TABLE_NAME = aws_dynamodb_table.audit-table.name + AUDIT_TABLE_TTL_DAYS = 60 } } kms_key_arn = data.aws_kms_key.existing_lambda_encryption_key.arn @@ -320,7 +320,7 @@ resource "aws_cloudwatch_log_group" "file_name_processor_log_group" { } resource "aws_cloudwatch_log_metric_filter" "file_name_processor_error_logs" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 name = "${local.short_prefix}-FilenameProcessorErrorLogsFilter" pattern = "%\\[ERROR\\]%" @@ -334,7 +334,7 @@ resource "aws_cloudwatch_log_metric_filter" "file_name_processor_error_logs" { } resource "aws_cloudwatch_metric_alarm" "file_name_processor_error_alarm" { - count = var.batch_error_notifications_enabled ? 1 : 0 + count = var.batch_error_notifications_enabled ? 1 : 0 alarm_name = "${local.short_prefix}-file-name-processor-lambda-error" comparison_operator = "GreaterThanOrEqualToThreshold" diff --git a/terraform/oas.yaml b/terraform/oas.yaml index 665e967e5..409c37574 100644 --- a/terraform/oas.yaml +++ b/terraform/oas.yaml @@ -13,9 +13,9 @@ paths: httpMethod: "GET" timeoutInMillis: 30000 type: "AWS_PROXY" - + responses: - '201': + "201": description: Get status content: application/fhir+json: @@ -32,12 +32,12 @@ paths: timeoutInMillis: 30000 type: "AWS_PROXY" responses: - '201': + "201": description: An Immunisation update event. Create new resource if it doesn't exist headers: Location: $ref: "#/components/headers/Location" - '200': + "200": description: An Immunisation update event get: x-amazon-apigateway-integration: @@ -48,7 +48,7 @@ paths: timeoutInMillis: 30000 type: "AWS_PROXY" responses: - '200': + "200": description: An Immunisation get event content: application/fhir+json: @@ -63,7 +63,7 @@ paths: timeoutInMillis: 30000 type: "AWS_PROXY" responses: - '200': + "200": description: An Immunisation delete event /Immunization: @@ -99,7 +99,7 @@ paths: schema: type: string responses: - '201': + "201": description: An Immunisation search event content: application/fhir+json: @@ -114,7 +114,7 @@ paths: timeoutInMillis: 30000 type: "AWS_PROXY" responses: - '201': + "201": description: An Immunisation post event headers: Location: @@ -151,7 +151,7 @@ paths: schema: type: string responses: - '201': + "201": description: An Immunisation search event content: application/fhir+json: @@ -164,7 +164,7 @@ paths: in: path required: true schema: - type: string + type: string x-amazon-apigateway-integration: uri: "${not_found.lambda_arn}" payloadFormatVersion: "1.0" @@ -173,7 +173,7 @@ paths: timeoutInMillis: 30000 type: "AWS_PROXY" responses: - '404': + "404": description: Not Found content: application/fhir+json: @@ -186,4 +186,3 @@ components: schema: type: string example: "https://int.api.service.nhs.uk/immunisation-fhir-api/Immunization/6c574dae-2e03-4dc7-87da-2b539a71a918" - diff --git a/terraform/policies/aws_sns_topic.json b/terraform/policies/aws_sns_topic.json index e8c90b345..ed8c29e42 100644 --- a/terraform/policies/aws_sns_topic.json +++ b/terraform/policies/aws_sns_topic.json @@ -3,12 +3,8 @@ "Statement": [ { "Effect": "Allow", - "Action": [ - "sns:Publish" - ], - "Resource": [ - "arn:aws:sns:*:*:${aws_sns_topic_name}" - ] + "Action": ["sns:Publish"], + "Resource": ["arn:aws:sns:*:*:${aws_sns_topic_name}"] } ] } diff --git a/terraform/policies/aws_sqs_queue.json b/terraform/policies/aws_sqs_queue.json index 0ad600a04..3716c03c0 100644 --- a/terraform/policies/aws_sqs_queue.json +++ b/terraform/policies/aws_sqs_queue.json @@ -1,15 +1,10 @@ { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "sqs:SendMessage" - ], - "Resource": [ - "arn:aws:sqs:*:*:${aws_sqs_queue_name}" - ] - } - ] - } - \ No newline at end of file + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sqs:SendMessage"], + "Resource": ["arn:aws:sqs:*:*:${aws_sqs_queue_name}"] + } + ] +} diff --git a/terraform/policies/dynamo_key_access.json b/terraform/policies/dynamo_key_access.json index 5b39958b6..b3ba30cd8 100644 --- a/terraform/policies/dynamo_key_access.json +++ b/terraform/policies/dynamo_key_access.json @@ -1,16 +1,10 @@ { - "Version" : "2012-10-17", - "Statement" : [ - { - "Effect" : "Allow", - "Action" : [ - "kms:Encrypt", - "kms:Decrypt", - "kms:GenerateDataKey*" - ], - "Resource" : [ - "${dynamo_encryption_key}" - ] - } - ] - } + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey*"], + "Resource": ["${dynamo_encryption_key}"] + } + ] +} diff --git a/terraform/policies/dynamodb_stream.json b/terraform/policies/dynamodb_stream.json index 5496b64f1..02c5c3483 100644 --- a/terraform/policies/dynamodb_stream.json +++ b/terraform/policies/dynamodb_stream.json @@ -1,16 +1,15 @@ { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "dynamodb:GetRecords", - "dynamodb:GetShardIterator", - "dynamodb:DescribeStream", - "dynamodb:ListStreams" - ], - "Resource": "arn:aws:dynamodb:*:*:table/${dynamodb_table_name}/stream/*" - } - ] - } - \ No newline at end of file + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:DescribeStream", + "dynamodb:ListStreams" + ], + "Resource": "arn:aws:dynamodb:*:*:table/${dynamodb_table_name}/stream/*" + } + ] +} diff --git a/terraform/policies/lambda_to_sqs.json b/terraform/policies/lambda_to_sqs.json index e9132230b..9a63bf721 100644 --- a/terraform/policies/lambda_to_sqs.json +++ b/terraform/policies/lambda_to_sqs.json @@ -1,12 +1,10 @@ { - "Version" : "2012-10-17", - "Statement" : [ - { - "Effect" : "Allow", - "Action" : [ - "sqs:SendMessage" - ], - "Resource" : "arn:aws:sqs:eu-west-2:${local_account}:${queue_prefix}-ack-metadata-queue.fifo" + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sqs:SendMessage"], + "Resource": "arn:aws:sqs:eu-west-2:${local_account}:${queue_prefix}-ack-metadata-queue.fifo" } - ] + ] } diff --git a/terraform/policies/log_kinesis.json b/terraform/policies/log_kinesis.json index ab52907ed..bd2b8b696 100644 --- a/terraform/policies/log_kinesis.json +++ b/terraform/policies/log_kinesis.json @@ -1,13 +1,10 @@ { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "firehose:PutRecord", - "firehose:PutRecordBatch" - ], - "Resource": "arn:aws:firehose:*:*:deliverystream/${kinesis_stream_name}" - } - ] - } + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["firehose:PutRecord", "firehose:PutRecordBatch"], + "Resource": "arn:aws:firehose:*:*:deliverystream/${kinesis_stream_name}" + } + ] +} diff --git a/terraform/s3_dq_reports.tf b/terraform/s3_dq_reports.tf index 3a44abd78..d19243d8b 100644 --- a/terraform/s3_dq_reports.tf +++ b/terraform/s3_dq_reports.tf @@ -1,7 +1,7 @@ # Create s3 Bucket with conditional destroy for pr environments resource "aws_s3_bucket" "data_quality_reports_bucket" { - bucket = "imms-${local.resource_scope}-data-quality-reports" - force_destroy = local.is_temp + bucket = "imms-${local.resource_scope}-data-quality-reports" + force_destroy = local.is_temp } @@ -47,8 +47,8 @@ resource "aws_iam_policy" "s3_dq_access" { Version = "2012-10-17" Statement = [ { - Effect = "Allow" - Action = ["s3:PutObject"] + Effect = "Allow" + Action = ["s3:PutObject"] Resource = [ aws_s3_bucket.data_quality_reports_bucket.arn, "${aws_s3_bucket.data_quality_reports_bucket.arn}/*" @@ -74,8 +74,8 @@ resource "aws_s3_bucket_policy" "data_quality_bucket_policy" { } Action = "s3:*" Resource = [ - aws_s3_bucket.data_quality_reports_bucket.arn, - "${aws_s3_bucket.data_quality_reports_bucket.arn}/*" + aws_s3_bucket.data_quality_reports_bucket.arn, + "${aws_s3_bucket.data_quality_reports_bucket.arn}/*" ] Condition = { Bool = { diff --git a/terraform/shared.tf b/terraform/shared.tf index 188a2fc76..ba977e093 100644 --- a/terraform/shared.tf +++ b/terraform/shared.tf @@ -1,8 +1,8 @@ # Define locals for shared lambdas locals { - shared_dir = abspath("${path.root}/../lambdas/shared") + shared_dir = abspath("${path.root}/../lambdas/shared") - shared_files = fileset(local.shared_dir, "**") + shared_files = fileset(local.shared_dir, "**") - shared_dir_sha = sha1(join("", [for f in local.shared_files : filesha1("${local.shared_dir}/${f}")])) + shared_dir_sha = sha1(join("", [for f in local.shared_files : filesha1("${local.shared_dir}/${f}")])) } diff --git a/terraform_aws_backup/aws-backup-destination/README.md b/terraform_aws_backup/aws-backup-destination/README.md index 34d9c9024..10e01514b 100644 --- a/terraform_aws_backup/aws-backup-destination/README.md +++ b/terraform_aws_backup/aws-backup-destination/README.md @@ -4,18 +4,18 @@ The AWS Backup Module helps automates the setup of AWS Backup resources in a des ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [account\_id](#input\_account\_id) | The id of the account that the vault will be in | `string` | n/a | yes | -| [changeable\_for\_days](#input\_changeable\_for\_days) | How long you want the vault lock to be changeable for, only applies to compliance mode. This value is expressed in days no less than 3 and no greater than 36,500; otherwise, an error will return. | `number` | `14` | no | -| [enable\_vault\_protection](#input\_enable\_vault\_protection) | Flag which controls if the vault lock is enabled | `bool` | `false` | no | -| [kms\_key](#input\_kms\_key) | The KMS key used to secure the vault | `string` | n/a | yes | -| [region](#input\_region) | The region we should be operating in | `string` | `"eu-west-2"` | no | -| [source\_account\_id](#input\_source\_account\_id) | The id of the account that backups will come from | `string` | n/a | yes | -| [source\_account\_name](#input\_source\_account\_name) | The name of the account that backups will come from | `string` | n/a | yes | -| [vault\_lock\_max\_retention\_days](#input\_vault\_lock\_max\_retention\_days) | The maximum retention period that the vault retains its recovery points | `number` | `365` | no | -| [vault\_lock\_min\_retention\_days](#input\_vault\_lock\_min\_retention\_days) | The minimum retention period that the vault retains its recovery points | `number` | `365` | no | -| [vault\_lock\_type](#input\_vault\_lock\_type) | The type of lock that the vault should be, will default to governance | `string` | `"governance"` | no | +| Name | Description | Type | Default | Required | +| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | :------: | +| [account_id](#input_account_id) | The id of the account that the vault will be in | `string` | n/a | yes | +| [changeable_for_days](#input_changeable_for_days) | How long you want the vault lock to be changeable for, only applies to compliance mode. This value is expressed in days no less than 3 and no greater than 36,500; otherwise, an error will return. | `number` | `14` | no | +| [enable_vault_protection](#input_enable_vault_protection) | Flag which controls if the vault lock is enabled | `bool` | `false` | no | +| [kms_key](#input_kms_key) | The KMS key used to secure the vault | `string` | n/a | yes | +| [region](#input_region) | The region we should be operating in | `string` | `"eu-west-2"` | no | +| [source_account_id](#input_source_account_id) | The id of the account that backups will come from | `string` | n/a | yes | +| [source_account_name](#input_source_account_name) | The name of the account that backups will come from | `string` | n/a | yes | +| [vault_lock_max_retention_days](#input_vault_lock_max_retention_days) | The maximum retention period that the vault retains its recovery points | `number` | `365` | no | +| [vault_lock_min_retention_days](#input_vault_lock_min_retention_days) | The minimum retention period that the vault retains its recovery points | `number` | `365` | no | +| [vault_lock_type](#input_vault_lock_type) | The type of lock that the vault should be, will default to governance | `string` | `"governance"` | no | ## Example diff --git a/terraform_aws_backup/aws-backup-source/README.md b/terraform_aws_backup/aws-backup-source/README.md index 6b9a40940..3f1b8bdb6 100644 --- a/terraform_aws_backup/aws-backup-source/README.md +++ b/terraform_aws_backup/aws-backup-source/README.md @@ -4,23 +4,23 @@ The AWS Backup Module helps automates the setup of AWS Backup resources in a sou ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [backup\_copy\_vault\_account\_id](#input\_backup\_copy\_vault\_account\_id) | The account id of the destination backup vault for allowing restores back into the source account. | `string` | `""` | no | -| [backup\_copy\_vault\_arn](#input\_backup\_copy\_vault\_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no | -| [backup\_plan\_config](#input\_backup\_plan\_config) | Configuration for backup plans |
object({
selection_tag = string
compliance_resource_types = list(string)
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = optional(number)
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
}))
})
|
{
"compliance_resource_types": [
"S3"
],
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"enable_continuous_backup": true,
"lifecycle": {
"delete_after": 35
},
"name": "point_in_time_recovery",
"schedule": "cron(0 5 * * ? *)"
}
],
"selection_tag": "BackupLocal"
}
| no | -| [backup\_plan\_config\_dynamodb](#input\_backup\_plan\_config\_dynamodb) | Configuration for backup plans with dynamodb |
object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
})
|
{
"compliance_resource_types": [
"DynamoDB"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "dynamodb_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "dynamodb_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "dynamodb_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupDynamoDB"
}
| no | -| [bootstrap\_kms\_key\_arn](#input\_bootstrap\_kms\_key\_arn) | The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic. | `string` | n/a | yes | -| [environment\_name](#input\_environment\_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes | -| [notifications\_target\_email\_address](#input\_notifications\_target\_email\_address) | The email address to which backup notifications will be sent via SNS. | `string` | `""` | no | -| [project\_name](#input\_project\_name) | The name of the project this relates to. | `string` | n/a | yes | -| [reports\_bucket](#input\_reports\_bucket) | Bucket to drop backup reports into | `string` | n/a | yes | -| [restore\_testing\_plan\_algorithm](#input\_restore\_testing\_plan\_algorithm) | Algorithm of the Recovery Selection Point | `string` | `"LATEST_WITHIN_WINDOW"` | no | -| [restore\_testing\_plan\_recovery\_point\_types](#input\_restore\_testing\_plan\_recovery\_point\_types) | Recovery Point Types | `list(string)` |
[
"SNAPSHOT"
]
| no | -| [restore\_testing\_plan\_scheduled\_expression](#input\_restore\_testing\_plan\_scheduled\_expression) | Scheduled Expression of Recovery Selection Point | `string` | `"cron(0 1 ? * SUN *)"` | no | -| [restore\_testing\_plan\_selection\_window\_days](#input\_restore\_testing\_plan\_selection\_window\_days) | Selection window days | `number` | `7` | no | -| [restore\_testing\_plan\_start\_window](#input\_restore\_testing\_plan\_start\_window) | Start window from the scheduled time during which the test should start | `number` | `1` | no | -| [terraform\_role\_arn](#input\_terraform\_role\_arn) | ARN of Terraform role used to deploy to account | `string` | n/a | yes | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [backup_copy_vault_account_id](#input_backup_copy_vault_account_id) | The account id of the destination backup vault for allowing restores back into the source account. | `string` | `""` | no | +| [backup_copy_vault_arn](#input_backup_copy_vault_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no | +| [backup_plan_config](#input_backup_plan_config) | Configuration for backup plans |
object({
selection_tag = string
compliance_resource_types = list(string)
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = optional(number)
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
}))
})
|
{
"compliance_resource_types": [
"S3"
],
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"enable_continuous_backup": true,
"lifecycle": {
"delete_after": 35
},
"name": "point_in_time_recovery",
"schedule": "cron(0 5 * * ? *)"
}
],
"selection_tag": "BackupLocal"
}
| no | +| [backup_plan_config_dynamodb](#input_backup_plan_config_dynamodb) | Configuration for backup plans with dynamodb |
object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
})
|
{
"compliance_resource_types": [
"DynamoDB"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "dynamodb_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "dynamodb_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "dynamodb_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupDynamoDB"
}
| no | +| [bootstrap_kms_key_arn](#input_bootstrap_kms_key_arn) | The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic. | `string` | n/a | yes | +| [environment_name](#input_environment_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes | +| [notifications_target_email_address](#input_notifications_target_email_address) | The email address to which backup notifications will be sent via SNS. | `string` | `""` | no | +| [project_name](#input_project_name) | The name of the project this relates to. | `string` | n/a | yes | +| [reports_bucket](#input_reports_bucket) | Bucket to drop backup reports into | `string` | n/a | yes | +| [restore_testing_plan_algorithm](#input_restore_testing_plan_algorithm) | Algorithm of the Recovery Selection Point | `string` | `"LATEST_WITHIN_WINDOW"` | no | +| [restore_testing_plan_recovery_point_types](#input_restore_testing_plan_recovery_point_types) | Recovery Point Types | `list(string)` |
[
"SNAPSHOT"
]
| no | +| [restore_testing_plan_scheduled_expression](#input_restore_testing_plan_scheduled_expression) | Scheduled Expression of Recovery Selection Point | `string` | `"cron(0 1 ? * SUN *)"` | no | +| [restore_testing_plan_selection_window_days](#input_restore_testing_plan_selection_window_days) | Selection window days | `number` | `7` | no | +| [restore_testing_plan_start_window](#input_restore_testing_plan_start_window) | Start window from the scheduled time during which the test should start | `number` | `1` | no | +| [terraform_role_arn](#input_terraform_role_arn) | ARN of Terraform role used to deploy to account | `string` | n/a | yes | ## Example diff --git a/tests/test_crud_immunisation_api.py b/tests/test_crud_immunisation_api.py index 8be39553b..a96a39f1e 100644 --- a/tests/test_crud_immunisation_api.py +++ b/tests/test_crud_immunisation_api.py @@ -8,11 +8,11 @@ from .immunisation_api import ImmunisationApi, parse_location -def create_an_imms_obj(imms_id: str = str(uuid.uuid4()), nhs_number=valid_nhs_number1) -> dict: +def create_an_imms_obj(imms_id: str = None, nhs_number=None) -> dict: imms = copy.deepcopy(load_example("Immunization/POST-COVID19-Immunization.json")) - imms["id"] = imms_id + imms["id"] = imms_id or str(uuid.uuid4()) imms["identifier"][0]["value"] = str(uuid.uuid4()) - imms["contained"][1]["identifier"][0]["value"] = nhs_number + imms["contained"][1]["identifier"][0]["value"] = nhs_number or valid_nhs_number1 return imms