From 19922168544d715ea902e73d9043af8b912ca9c9 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Fri, 20 Jun 2025 10:22:02 +0100 Subject: [PATCH 01/50] Enable cross-account backups * Allow backup account limited usage of source account KMS key to copy backups * Allow source account to use backup account KMS key to restore snapshots from there Jira-Issue: MAV-1158 --- terraform/app/kms.tf | 27 +++++++++++++++++++++++++++ terraform/app/variables.tf | 7 +++++++ terraform/backup/destination/main.tf | 27 +++++++++++++++++++++++++++ terraform/backup/source/main.tf | 7 +++---- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/terraform/app/kms.tf b/terraform/app/kms.tf index 64a8a9b0ff..752bb752d5 100644 --- a/terraform/app/kms.tf +++ b/terraform/app/kms.tf @@ -11,6 +11,33 @@ resource "aws_kms_key" "rds_cluster" { } Action = "kms:*" Resource = "*" + }, { + Sid = "AllowBackupAccount" + Effect = "Allow" + Principal = { + AWS = ["arn:aws:iam::${var.backup_account_id}:root"] + } + "Action" : [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ], + "Resource" : "*" + }, { + Sid = "Allow attachment of persistent resources" + Effect = "Allow" + Principal = { + AWS = ["arn:aws:iam::${var.backup_account_id}:root"] + } + "Action" : [ + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ], + "Resource" : "*", + "Condition" : { "Bool" : { "kms:GrantIsForAWSResource" : true } } } ] }) diff --git a/terraform/app/variables.tf b/terraform/app/variables.tf index e186bb04a1..eb8601611d 100644 --- a/terraform/app/variables.tf +++ b/terraform/app/variables.tf @@ -239,6 +239,13 @@ variable "enable_backup_to_vault" { nullable = false } +variable "backup_account_id" { + type = string + default = "904214613099" + description = "The AWS account ID of the backup account" + nullable = false +} + locals { db_instances = { "primary-1" = { diff --git a/terraform/backup/destination/main.tf b/terraform/backup/destination/main.tf index 8b3b850fd0..f0f629890f 100644 --- a/terraform/backup/destination/main.tf +++ b/terraform/backup/destination/main.tf @@ -42,6 +42,33 @@ resource "aws_kms_key" "destination_backup_key" { } Action = "kms:*" Resource = "*" + }, { + Sid = "AllowRestoreToSourceAccount" + Effect = "Allow" + Principal = { + AWS = ["arn:aws:iam::${var.source_account_id}:root"] + } + "Action" : [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ], + "Resource" : "*" + }, { + Sid = "Allow attachment of persistent resources" + Effect = "Allow" + Principal = { + AWS = ["arn:aws:iam::${var.source_account_id}:root"] + } + "Action" : [ + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ], + "Resource" : "*", + "Condition" : { "Bool" : { "kms:GrantIsForAWSResource" : true } } } ] }) diff --git a/terraform/backup/source/main.tf b/terraform/backup/source/main.tf index 5f4bf5c36c..3b71cc008f 100644 --- a/terraform/backup/source/main.tf +++ b/terraform/backup/source/main.tf @@ -111,10 +111,9 @@ module "source" { ], "rules" : [ { - # Cross-account copying will be enabled in MAV-1158 - # "copy_action" : { - # "delete_after" : 60 - # }, + "copy_action" : { + "delete_after" : var.backup_retention_period + }, "lifecycle" : { "delete_after" : var.backup_retention_period }, From d8b3d021168d60999e4a9a3ede23a385c63c4b3d Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Mon, 23 Jun 2025 14:29:01 +0100 Subject: [PATCH 02/50] Update disaster recovery documentation Jira-Issue: MAV-1158 --- docs/disaster-recovery.md | 41 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/disaster-recovery.md b/docs/disaster-recovery.md index aa6ccee3c4..440da68d5f 100644 --- a/docs/disaster-recovery.md +++ b/docs/disaster-recovery.md @@ -41,6 +41,34 @@ aws rds modify-db-cluster \ Deploy to your restored environment as described in [Terraform: Local deployment](terraform.md#local-deployment). +## Restoring a production database from a vault backup in the same account + +- In the AWS Backup console, go to the Vaults page and select a suitable recovery point. +- Click on Actions > Restore +- As DB cluster identifier, enter the desired name for the new cluster. It must match the `cluster_identifier` in the + `terraform/app/rds.tf` file. +- Restore the backup to a new RDS cluster and wait until the cluster is available. +- Modify the new cluster to use AWS managed credentials instead of self-managed credentials. +- Import the newly created cluster into Terraform by running `terraform import aws_rds_cluster.core CLUSTER_ID_OF_NEW_CLUSTER`. + +## Restoring a production database from a vault backup in the backup account + +- Go to the AWS Backup console in the backup account and select a recovery point to be restored. +- Click on Actions > Copy > Copy back to source account. +- Once it's copied back to the source account, follow [Restoring a production database from a vault in the same account](#restoring-a-production-database-from-a-vault-backup-in-the-same-account) to restore the database from the copied snapshot. + +## Recreate infrastructure from scratch in a new AWS account + +If you need to recreate the infrastructure from scratch in a new AWS account, follow these steps: + +- In the new account, create a new terraform environment, following [Terraform: Creating a new + environment](terraform.md#creating-a-new-environment). +- From the AWS console, copy over the latest snapshot of the production database to the new account. Select the new account as target for the copy action. +- Follow [Restoring a production database from a vault in the same account](#restoring-a-production-database-from-a-vault-backup-in-the-same-account) to restore the database from the copied snapshot. +- Update AWS account IDs in the terraform `variables.tf` files and in the GitHub workflows. +- Create the required IAM resources for the GitHub workflows by running `terraform apply` for the `terraform/accounts` module. +- Run the `deploy.yml` workflow to deploy the new infrastructure into the new account. + ## Getting a local dump of an Aurora DB You need Postgres 16+ to connect to the Aurora DB. @@ -195,16 +223,3 @@ RAILS_ENV=staging bin/bundle exec \ EXPORT_PASSWORD=secure \ node ./script/encrypt_xlsx.mjs ``` - -## Set up a new AWS account from scratch - -### Create a new IAM role for GitHub workflows - -In the AWS IAM console, create a new role for the GitHub workflows to assume. - -- Create a custom policy from `terraform/resources/github_actions_policy.json`. -- Define the trust policy either as `github_role_production_trust_policy.json` or `github_role_development_trust_policy.json` depending on whether the new account is a production account or not. - -- Attach the managed policies - - `ReadOnlyAccess` - - `ResourceGroupsTaggingAPITagUntagSupportedResources` to the role. From d84127fae7a881028aba022ae1544c8371385788 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Wed, 25 Jun 2025 08:46:39 +0100 Subject: [PATCH 03/50] Update recovery process documentation --- docs/disaster-recovery.md | 13 +++++++------ terraform/app/rds.tf | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/disaster-recovery.md b/docs/disaster-recovery.md index 440da68d5f..c35d7df4c8 100644 --- a/docs/disaster-recovery.md +++ b/docs/disaster-recovery.md @@ -44,15 +44,16 @@ Deploy to your restored environment as described in [Terraform: Local deployment ## Restoring a production database from a vault backup in the same account - In the AWS Backup console, go to the Vaults page and select a suitable recovery point. -- Click on Actions > Restore -- As DB cluster identifier, enter the desired name for the new cluster. It must match the `cluster_identifier` in the - `terraform/app/rds.tf` file. -- Restore the backup to a new RDS cluster and wait until the cluster is available. -- Modify the new cluster to use AWS managed credentials instead of self-managed credentials. -- Import the newly created cluster into Terraform by running `terraform import aws_rds_cluster.core CLUSTER_ID_OF_NEW_CLUSTER`. +- Copy the arn of the DB snapshot and add it to the `snapshot_identifier` in the `aws_rds_cluster core` resource block in + `terraform/app/rds.tf`. +- Recreate the infrastructure by running `terraform apply`. +- After the infrastructure is created, remove the `snapshot_identifier` line from the `aws_rds_cluster core` resource block again ## Restoring a production database from a vault backup in the backup account +- Verify that the AWS Backup vault still exists in the production account. If it doesn't, you will need to restore the vault first. + - sdf + - asdf - Go to the AWS Backup console in the backup account and select a recovery point to be restored. - Click on Actions > Copy > Copy back to source account. - Once it's copied back to the source account, follow [Restoring a production database from a vault in the same account](#restoring-a-production-database-from-a-vault-backup-in-the-same-account) to restore the database from the copied snapshot. diff --git a/terraform/app/rds.tf b/terraform/app/rds.tf index 15db63aa4c..f67a156173 100644 --- a/terraform/app/rds.tf +++ b/terraform/app/rds.tf @@ -47,7 +47,6 @@ resource "aws_rds_cluster" "core" { kms_key_id = aws_kms_key.rds_cluster.arn storage_encrypted = true manage_master_user_password = true - enable_http_endpoint = true deletion_protection = true allow_major_version_upgrade = true preferred_backup_window = "01:00-01:30" From 3f4bfcf1ad86b55386a7640094cba967bd3907f8 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Wed, 25 Jun 2025 09:35:55 +0100 Subject: [PATCH 04/50] Fix destroy workflow --- .github/workflows/destroy-infrastructure.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/destroy-infrastructure.yml b/.github/workflows/destroy-infrastructure.yml index b8342c7a6e..6ffc868f95 100644 --- a/.github/workflows/destroy-infrastructure.yml +++ b/.github/workflows/destroy-infrastructure.yml @@ -47,10 +47,9 @@ jobs: run: | set -e terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - if terraform state list | grep -q 'aws_rds_cluster.aurora_cluster'; then - echo "DB cluster exsits: removing delete protection" - CLUSTER_IDENTIFIER=$(grep -oP 'db_cluster\s*=\s*"\K[^"]+' env/${{ inputs.environment }}.tfvars) - aws rds modify-db-cluster --db-cluster-identifier "$CLUSTER_IDENTIFIER" --no-deletion-protection + if terraform state list | grep -q 'aws_rds_cluster.core'; then + echo "DB cluster exists: removing delete protection" + aws rds modify-db-cluster --db-cluster-identifier mavis-${{ inputs.environment }} --no-deletion-protection echo "DB cluster delete protection removed: proceeding to delete stage" else echo "DB cluster not in state: proceeding to delete stage" From 8f90232048198ff1bca187a1d25d35b1734a3b49 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Wed, 25 Jun 2025 11:30:42 +0100 Subject: [PATCH 05/50] Update documentation --- docs/disaster-recovery.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/disaster-recovery.md b/docs/disaster-recovery.md index c35d7df4c8..2f7ef3250e 100644 --- a/docs/disaster-recovery.md +++ b/docs/disaster-recovery.md @@ -45,15 +45,14 @@ Deploy to your restored environment as described in [Terraform: Local deployment - In the AWS Backup console, go to the Vaults page and select a suitable recovery point. - Copy the arn of the DB snapshot and add it to the `snapshot_identifier` in the `aws_rds_cluster core` resource block in - `terraform/app/rds.tf`. + [rds.tf](../terraform/app/rds.tf). - Recreate the infrastructure by running `terraform apply`. - After the infrastructure is created, remove the `snapshot_identifier` line from the `aws_rds_cluster core` resource block again ## Restoring a production database from a vault backup in the backup account -- Verify that the AWS Backup vault still exists in the production account. If it doesn't, you will need to restore the vault first. - - sdf - - asdf +- Verify that the AWS Backup vault still exists in the production account. + - If it doesn't, you will need to restore the vault first by running [deploy-backup-infrastructure.yml](../.github/workflows/deploy-backup-infrastructure.yml) workflow. - Go to the AWS Backup console in the backup account and select a recovery point to be restored. - Click on Actions > Copy > Copy back to source account. - Once it's copied back to the source account, follow [Restoring a production database from a vault in the same account](#restoring-a-production-database-from-a-vault-backup-in-the-same-account) to restore the database from the copied snapshot. @@ -64,11 +63,12 @@ If you need to recreate the infrastructure from scratch in a new AWS account, fo - In the new account, create a new terraform environment, following [Terraform: Creating a new environment](terraform.md#creating-a-new-environment). + - Potentially, you might need to change the S3 bucket name +- Update AWS account IDs in all environment variables terraform files and in the GitHub workflows. +- Create the required IAM resources for the GitHub workflows by running `terraform apply` for the `terraform/accounts` module. +- Create a new AWS Backup vault by running [deploy-backup-infrastructure.yml](../.github/workflows/deploy-backup-infrastructure.yml) workflow. - From the AWS console, copy over the latest snapshot of the production database to the new account. Select the new account as target for the copy action. - Follow [Restoring a production database from a vault in the same account](#restoring-a-production-database-from-a-vault-backup-in-the-same-account) to restore the database from the copied snapshot. -- Update AWS account IDs in the terraform `variables.tf` files and in the GitHub workflows. -- Create the required IAM resources for the GitHub workflows by running `terraform apply` for the `terraform/accounts` module. -- Run the `deploy.yml` workflow to deploy the new infrastructure into the new account. ## Getting a local dump of an Aurora DB From 922d5ba5f83f8b5346f795e50c2015d9b41af482 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 26 Jun 2025 11:49:46 +0100 Subject: [PATCH 06/50] Validate DB connection in internal healthcheck - Try to initialize a new connection - ActiveRecord persists connections after credential change - New connections fails after credential change - This means that while old connections exist we will identify that the credentials have changed and trigger a redeployment of containers - This avoids any disturbance to user traffic and avoids any `500` error messages - Additionally increase the frequency of ELB healthchecks to limit inpact to users in any potential future situations when a container becomes unhealthy --- Dockerfile | 2 +- bin/internal_healthcheck | 9 +++++++++ terraform/app/ecs.tf | 4 ++-- terraform/app/loadbalancer.tf | 8 ++++---- 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100755 bin/internal_healthcheck diff --git a/Dockerfile b/Dockerfile index 3614098fc1..1184e19ff1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /rails # Install base packages RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips libicu-dev postgresql-client && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips libicu-dev postgresql-client jq && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment diff --git a/bin/internal_healthcheck b/bin/internal_healthcheck new file mode 100755 index 0000000000..1d7ca52a81 --- /dev/null +++ b/bin/internal_healthcheck @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +export PGPASSWORD="$(echo $DB_CREDENTIALS | jq -r .password)" +psql -h "$DB_HOST" -d "$DB_NAME" -U "$(echo $DB_CREDENTIALS | jq -r .username)" -c "select 1" || { + echo "DB connection could not be established: Internal healthcheck failed."; exit 1; +} +curl -f "$1" || { + echo "DB connection could not be established, but $1 did not return a 200 response."; exit 2; +} diff --git a/terraform/app/ecs.tf b/terraform/app/ecs.tf index 75f98d9855..ea64a14688 100644 --- a/terraform/app/ecs.tf +++ b/terraform/app/ecs.tf @@ -31,7 +31,7 @@ module "web_service" { task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name region = var.region - health_check_command = ["CMD-SHELL", "curl -f http://localhost:4000/health/database || exit 1"] + health_check_command = ["CMD-SHELL", "./bin/internal_healthcheck http://localhost:4000/health/database"] } network_params = { subnets = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id] @@ -70,7 +70,7 @@ module "good_job_service" { task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name region = var.region - health_check_command = ["CMD-SHELL", "curl -f http://localhost:4000/status/connected || exit 1"] + health_check_command = ["CMD-SHELL", "./bin/internal_healthcheck http://localhost:4000/status/connected"] } network_params = { subnets = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id] diff --git a/terraform/app/loadbalancer.tf b/terraform/app/loadbalancer.tf index b39f3469be..4ecb0cc164 100644 --- a/terraform/app/loadbalancer.tf +++ b/terraform/app/loadbalancer.tf @@ -82,8 +82,8 @@ resource "aws_lb_target_group" "blue" { protocol = "HTTP" port = "traffic-port" matcher = "200" - interval = 10 - timeout = 5 + interval = 5 + timeout = 4 healthy_threshold = 2 unhealthy_threshold = 2 } @@ -100,8 +100,8 @@ resource "aws_lb_target_group" "green" { protocol = "HTTP" port = "traffic-port" matcher = "200" - interval = 10 - timeout = 5 + interval = 5 + timeout = 4 healthy_threshold = 2 unhealthy_threshold = 2 } From 7ebf2b8465daf72368fc659f6fac0e8495722e13 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Thu, 3 Jul 2025 13:39:57 +0100 Subject: [PATCH 07/50] Upgrade major AWS terraform provider version * No relevant breaking changes are mentioned in the release notes --- terraform/account/main.tf | 2 +- terraform/app/main.tf | 2 +- terraform/app/modules/dms/main.tf | 2 +- terraform/app/modules/dns/main.tf | 2 +- terraform/app/modules/ecs_service/main.tf | 2 +- terraform/app/modules/vpc_endpoint/main.tf | 2 +- terraform/backup/destination-bootstrap/main.tf | 2 +- terraform/backup/destination/main.tf | 2 +- terraform/backup/source/main.tf | 2 +- terraform/bootstrap/main.tf | 2 +- terraform/modules/s3/main.tf | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/terraform/account/main.tf b/terraform/account/main.tf index c0ee646604..cb15d4dab1 100644 --- a/terraform/account/main.tf +++ b/terraform/account/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } diff --git a/terraform/app/main.tf b/terraform/app/main.tf index d493f22d05..412cdc1c98 100644 --- a/terraform/app/main.tf +++ b/terraform/app/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } time = { source = "hashicorp/time" diff --git a/terraform/app/modules/dms/main.tf b/terraform/app/modules/dms/main.tf index a4087b11f3..86795ab677 100644 --- a/terraform/app/modules/dms/main.tf +++ b/terraform/app/modules/dms/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } time = { source = "hashicorp/time" diff --git a/terraform/app/modules/dns/main.tf b/terraform/app/modules/dns/main.tf index 21f0697b72..c9f2912631 100644 --- a/terraform/app/modules/dns/main.tf +++ b/terraform/app/modules/dns/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } diff --git a/terraform/app/modules/ecs_service/main.tf b/terraform/app/modules/ecs_service/main.tf index 4f2f35c408..1ab9bd2a21 100644 --- a/terraform/app/modules/ecs_service/main.tf +++ b/terraform/app/modules/ecs_service/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } diff --git a/terraform/app/modules/vpc_endpoint/main.tf b/terraform/app/modules/vpc_endpoint/main.tf index cb5c3ad99c..750c5ca8b8 100644 --- a/terraform/app/modules/vpc_endpoint/main.tf +++ b/terraform/app/modules/vpc_endpoint/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } diff --git a/terraform/backup/destination-bootstrap/main.tf b/terraform/backup/destination-bootstrap/main.tf index 4520092cfe..f763673db0 100644 --- a/terraform/backup/destination-bootstrap/main.tf +++ b/terraform/backup/destination-bootstrap/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } diff --git a/terraform/backup/destination/main.tf b/terraform/backup/destination/main.tf index 8b3b850fd0..029009a849 100644 --- a/terraform/backup/destination/main.tf +++ b/terraform/backup/destination/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } diff --git a/terraform/backup/source/main.tf b/terraform/backup/source/main.tf index 5f4bf5c36c..7c0117c39b 100644 --- a/terraform/backup/source/main.tf +++ b/terraform/backup/source/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } diff --git a/terraform/bootstrap/main.tf b/terraform/bootstrap/main.tf index 8d0324a566..db025b3157 100644 --- a/terraform/bootstrap/main.tf +++ b/terraform/bootstrap/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } diff --git a/terraform/modules/s3/main.tf b/terraform/modules/s3/main.tf index f2c2345bbf..0ddef25c0a 100644 --- a/terraform/modules/s3/main.tf +++ b/terraform/modules/s3/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.87" + version = "~> 6.2" } } } From 4ee4a152b6c8c92841c5d1dad0b9132b8686324c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 21:59:59 +0000 Subject: [PATCH 08/50] Bump flipper-ui from 1.3.4 to 1.3.5 Bumps [flipper-ui](https://github.com/flippercloud/flipper) from 1.3.4 to 1.3.5. - [Release notes](https://github.com/flippercloud/flipper/releases) - [Changelog](https://github.com/flippercloud/flipper/blob/main/Changelog.md) - [Commits](https://github.com/flippercloud/flipper/compare/v1.3.4...v1.3.5) --- updated-dependencies: - dependency-name: flipper-ui dependency-version: 1.3.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 13c04c2017..1b879cb16f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -252,14 +252,14 @@ GEM date_time_precision (>= 0.8) mime-types (>= 3.0) nokogiri (>= 1.11.4) - flipper (1.3.4) + flipper (1.3.5) concurrent-ruby (< 2) flipper-active_record (1.3.4) activerecord (>= 4.2, < 9) flipper (~> 1.3.4) - flipper-ui (1.3.4) + flipper-ui (1.3.5) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.3.4) + flipper (~> 1.3.5) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) From c9dae220ae509151fc3e47ea0a2aea93f812ddb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:00:15 +0000 Subject: [PATCH 09/50] Bump faker from 3.5.1 to 3.5.2 Bumps [faker](https://github.com/faker-ruby/faker) from 3.5.1 to 3.5.2. - [Release notes](https://github.com/faker-ruby/faker/releases) - [Changelog](https://github.com/faker-ruby/faker/blob/main/CHANGELOG.md) - [Commits](https://github.com/faker-ruby/faker/compare/v3.5.1...v3.5.2) --- updated-dependencies: - dependency-name: faker dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 13c04c2017..59a9ed96bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,7 +229,7 @@ GEM factory_bot_rails (6.5.0) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.5.1) + faker (3.5.2) i18n (>= 1.8.11, < 2) faraday (2.13.1) faraday-net_http (>= 2.0, < 3.5) From 46fb76653dcc650cdff0fc50200209b47bcef105 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:00:43 +0000 Subject: [PATCH 10/50] Bump solargraph from 0.55.4 to 0.56.0 Bumps [solargraph](https://github.com/castwide/solargraph) from 0.55.4 to 0.56.0. - [Changelog](https://github.com/castwide/solargraph/blob/master/CHANGELOG.md) - [Commits](https://github.com/castwide/solargraph/compare/v0.55.4...v0.56.0) --- updated-dependencies: - dependency-name: solargraph dependency-version: 0.56.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 13c04c2017..6b672ac80b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -618,7 +618,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - solargraph (0.55.4) + solargraph (0.56.0) backport (~> 1.2) benchmark (~> 0.4) bundler (~> 2.0) @@ -630,6 +630,7 @@ GEM observer (~> 0.1) ostruct (~> 0.6) parser (~> 3.0) + prism (~> 1.4) rbs (~> 3.3) reverse_markdown (~> 3.0) rubocop (~> 1.38) From 8927c8117eb82e5062ca745826c849b02a501afa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:15:10 +0000 Subject: [PATCH 11/50] Bump aws-sdk-s3 from 1.191.0 to 1.192.0 Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.191.0 to 1.192.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) --- updated-dependencies: - dependency-name: aws-sdk-s3 dependency-version: 1.192.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 50a1252540..ea5d8ed384 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -139,7 +139,7 @@ GEM aws-sdk-rds (1.283.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.191.0) + aws-sdk-s3 (1.192.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) From 6255c3079d525f65ee89c1caf4f0a471578a5f65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:14:36 +0000 Subject: [PATCH 12/50] Bump solargraph-rails from 1.1.0 to 1.2.0 Bumps [solargraph-rails](https://github.com/iftheshoefritz/solargraph-rails) from 1.1.0 to 1.2.0. - [Changelog](https://github.com/iftheshoefritz/solargraph-rails/blob/main/CHANGELOG.md) - [Commits](https://github.com/iftheshoefritz/solargraph-rails/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: solargraph-rails dependency-version: 1.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ea5d8ed384..fa3f44496e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -638,9 +638,9 @@ GEM tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) yard-solargraph (~> 0.1) - solargraph-rails (1.1.0) + solargraph-rails (1.2.0) activesupport - solargraph + solargraph (= 0.56.0) splunk-sdk-ruby (1.0.5) stackprof (0.2.27) stimulus-rails (1.3.4) From c907ab482c93cecad3205f8e48a46901891535d4 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 19 Jun 2025 14:20:10 +0100 Subject: [PATCH 13/50] Add side effects to vaccines We need to add support for storing the side effects of each vaccine so these can be displayed dynamically in the emails for the session reminders and the vaccination confirmations. Jira-Issue: MAV-1354 --- app/models/concerns/has_side_effects.rb | 29 +++++++++++++++++++ app/models/concerns/has_vaccine_methods.rb | 2 +- app/models/vaccine.rb | 3 ++ ...0619125105_add_side_effects_to_vaccines.rb | 12 ++++++++ db/schema.rb | 1 + spec/factories/vaccines.rb | 1 + spec/models/vaccine_spec.rb | 1 + 7 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/has_side_effects.rb create mode 100644 db/migrate/20250619125105_add_side_effects_to_vaccines.rb diff --git a/app/models/concerns/has_side_effects.rb b/app/models/concerns/has_side_effects.rb new file mode 100644 index 0000000000..6bdc593db7 --- /dev/null +++ b/app/models/concerns/has_side_effects.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module HasSideEffects + extend ActiveSupport::Concern + + included do + extend ArrayEnum + + array_enum side_effects: { + aching: 0, + dizziness: 1, + drowsy: 2, + feeling_sick: 3, + headache: 4, + high_temperature: 5, + irritable: 6, + loss_of_appetite: 8, + pain_in_arms: 9, + raised_temperature: 10, + rash: 11, + runny_blocked_nose: 12, + swelling: 13, + tiredness: 14, + unwell: 15 + } + + validates :side_effects, subset: side_effects.keys + end +end diff --git a/app/models/concerns/has_vaccine_methods.rb b/app/models/concerns/has_vaccine_methods.rb index 5cfc1f6166..efa3f17d0b 100644 --- a/app/models/concerns/has_vaccine_methods.rb +++ b/app/models/concerns/has_vaccine_methods.rb @@ -8,7 +8,7 @@ module HasVaccineMethods array_enum vaccine_methods: { injection: 0, nasal: 1 } - validates :vaccine_methods, subset: %w[injection nasal] + validates :vaccine_methods, subset: vaccine_methods.keys end def vaccine_method_injection? = vaccine_methods.include?("injection") diff --git a/app/models/vaccine.rb b/app/models/vaccine.rb index 4143c22eee..4120ec0ec3 100644 --- a/app/models/vaccine.rb +++ b/app/models/vaccine.rb @@ -11,6 +11,7 @@ # manufacturer :text not null # method :integer not null # nivs_name :text not null +# side_effects :integer default([]), not null, is an Array # snomed_product_code :string not null # snomed_product_term :string not null # created_at :datetime not null @@ -30,6 +31,8 @@ # fk_rails_... (programme_id => programmes.id) # class Vaccine < ApplicationRecord + include HasSideEffects + audited associated_with: :programme has_associated_audits diff --git a/db/migrate/20250619125105_add_side_effects_to_vaccines.rb b/db/migrate/20250619125105_add_side_effects_to_vaccines.rb new file mode 100644 index 0000000000..f3559567fa --- /dev/null +++ b/db/migrate/20250619125105_add_side_effects_to_vaccines.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddSideEffectsToVaccines < ActiveRecord::Migration[8.0] + def change + add_column :vaccines, + :side_effects, + :integer, + array: true, + default: [], + null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e385f7c1f..256eb74bd2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -842,6 +842,7 @@ t.text "nivs_name", null: false t.boolean "discontinued", default: false, null: false t.bigint "programme_id", null: false + t.integer "side_effects", default: [], null: false, array: true t.index ["manufacturer", "brand"], name: "index_vaccines_on_manufacturer_and_brand", unique: true t.index ["nivs_name"], name: "index_vaccines_on_nivs_name", unique: true t.index ["programme_id"], name: "index_vaccines_on_programme_id" diff --git a/spec/factories/vaccines.rb b/spec/factories/vaccines.rb index 8662f7fd35..4aa1baf5f7 100644 --- a/spec/factories/vaccines.rb +++ b/spec/factories/vaccines.rb @@ -11,6 +11,7 @@ # manufacturer :text not null # method :integer not null # nivs_name :text not null +# side_effects :integer default([]), not null, is an Array # snomed_product_code :string not null # snomed_product_term :string not null # created_at :datetime not null diff --git a/spec/models/vaccine_spec.rb b/spec/models/vaccine_spec.rb index 12ef8d66ba..db565a01d9 100644 --- a/spec/models/vaccine_spec.rb +++ b/spec/models/vaccine_spec.rb @@ -11,6 +11,7 @@ # manufacturer :text not null # method :integer not null # nivs_name :text not null +# side_effects :integer default([]), not null, is an Array # snomed_product_code :string not null # snomed_product_term :string not null # created_at :datetime not null From 4d6ab08634e0abff855c7ed103f697045a7309b6 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 19 Jun 2025 15:04:52 +0100 Subject: [PATCH 14/50] Expose vaccine side effects as personalisation This allows them to be included in emails we send out to users, either before a session for all programmes, or after a session for a vaccination record. Jira-Issue: MAV-1355 --- app/lib/govuk_notify_personalisation.rb | 19 ++++++++++++++++++- config/locales/en.yml | 16 ++++++++++++++++ spec/lib/govuk_notify_personalisation_spec.rb | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index 194a78a3be..a6c6106cc7 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -65,7 +65,8 @@ def to_h team_name:, team_phone:, today_or_date_of_vaccination:, - vaccination: + vaccination:, + vaccine_side_effects: }.compact end @@ -286,4 +287,20 @@ def vaccination programmes.count == 1 ? "vaccination" : "vaccinations" ].join(" ") end + + def vaccine_side_effects + side_effects = + if vaccination_record + vaccination_record.vaccine&.side_effects + elsif programmes.present? + Vaccine.where(programme: programmes).flat_map(&:side_effects) + end + + return if side_effects.nil? + + descriptions = + side_effects.map { Vaccine.human_enum_name(:side_effect, it) }.sort.uniq + + descriptions.map { "- #{it}" }.join("\n") + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 71767a5012..60135a5e14 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -231,6 +231,22 @@ en: injection: Injection nasal: Nasal spray nasal_injection: Nasal spray (or injection) + side_effects: + aching: an aching body + dizziness: dizziness + drowsy: feeling drowsy + feeling_sick: feeling sick + headache: a headache + high_temperature: a high temperature + irritable: feeling irritable + loss_of_appetite: loss of appetite + pain_in_arms: pain in the arms, hands, fingers + raised_temperature: a slightly raised temperature + rash: a rash + runny_blocked_nose: a runny or blocked nose + swelling: swelling or pain where the injection was given + tiredness: general tiredness + unwell: generally feeling unwell errors: models: class_import: diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index f674763f1b..8a50beb325 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -73,7 +73,8 @@ team_email: "organisation@example.com", team_name: "Organisation", team_phone: "01234 567890 (option 1)", - vaccination: "HPV vaccination" + vaccination: "HPV vaccination", + vaccine_side_effects: "" } ) end @@ -241,4 +242,19 @@ ) end end + + context "with vaccine side effects" do + before do + programmes.first.vaccines.first.update!(side_effects: %w[swelling unwell]) + end + + it do + expect(to_h).to match( + hash_including( + vaccine_side_effects: + "- generally feeling unwell\n- swelling or pain where the injection was given" + ) + ) + end + end end From 34f63284f7e8e59954a00f34a2c9642d69684903 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 19 Jun 2025 15:08:35 +0100 Subject: [PATCH 15/50] Seed vaccine side effects This adds the side effects for the vaccines for all programmes. Jira-Issue: MAV-1354 --- lib/tasks/vaccines.rake | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/lib/tasks/vaccines.rake b/lib/tasks/vaccines.rake index cd02f6622c..58ba87f378 100644 --- a/lib/tasks/vaccines.rake +++ b/lib/tasks/vaccines.rake @@ -26,6 +26,8 @@ namespace :vaccines do vaccine.snomed_product_term = data["snomed_product_term"] vaccine.programme = programme + vaccine.side_effects = side_effects_for(programme, data["method"]) + vaccine.save! next if vaccine.health_questions.exists? @@ -47,6 +49,61 @@ namespace :vaccines do end end +def side_effects_for(programme, method) + if programme.flu? + if method == "nasal" + %w[runny_blocked_nose headache tiredness loss_of_appetite] + else + %w[ + swelling + headache + high_temperature + feeling_sick + irritable + drowsy + loss_of_appetite + unwell + ] + end + elsif programme.hpv? + %w[ + swelling + headache + high_temperature + feeling_sick + irritable + drowsy + loss_of_appetite + unwell + ] + elsif programme.menacwy? + %w[ + drowsy + feeling_sick + headache + high_temperature + irritable + loss_of_appetite + rash + swelling + unwell + ] + elsif programme.td_ipv? + %w[ + drowsy + feeling_sick + headache + high_temperature + irritable + loss_of_appetite + swelling + unwell + ] + else + raise UnsupportedProgramme, programme + end +end + def create_flu_health_questions(vaccine) asthma = if vaccine.nasal? From 4d1b887ee4f7a40ebe8e4fbd9e84399ffdf39d46 Mon Sep 17 00:00:00 2001 From: Lakshmi Murugappan Date: Tue, 8 Jul 2025 11:48:35 +0100 Subject: [PATCH 16/50] Change session reminder template to point to generic reminder The new session reminder template accepts a `vaccine_side_effects` variable, allowing it to be customised for each programme. This change changes the template id to point to the new template. --- config/initializers/govuk_notify.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/govuk_notify.rb b/config/initializers/govuk_notify.rb index 9ca03c2a22..079dd8bd9f 100644 --- a/config/initializers/govuk_notify.rb +++ b/config/initializers/govuk_notify.rb @@ -17,7 +17,7 @@ "6410145f-dac1-46ba-82f3-a49cad0f66a6", session_clinic_initial_invitation: "fc99ac81-9eeb-4df8-9aa0-04f0eb48e37f", session_clinic_subsequent_invitation: "eee59c1b-3af4-4ccd-8653-940887066390", - session_school_reminder: "79e131b2-7816-46d0-9c74-ae14956dd77d", + session_school_reminder: "8b8a9566-bb03-4b3c-8abc-5bd5a4b8797d", triage_vaccination_at_clinic: "9faef718-bd76-4c30-93ea-fbe8584388a6", triage_vaccination_will_happen: "fa3c8dd5-4688-4b93-960a-1d422c4e5597", triage_vaccination_wont_happen: "d1faf47e-ccc3-4481-975b-1ec34211a21f", From 5bc6e4f43fcce03792fbf17cdbf29a1cb1fb802d Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Tue, 8 Jul 2025 12:46:39 +0100 Subject: [PATCH 17/50] Changes needed for successful terraform apply --- .../account/resources/iam_policy_DeployMavisResources.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terraform/account/resources/iam_policy_DeployMavisResources.json b/terraform/account/resources/iam_policy_DeployMavisResources.json index eb2f8e5176..9366660c7c 100644 --- a/terraform/account/resources/iam_policy_DeployMavisResources.json +++ b/terraform/account/resources/iam_policy_DeployMavisResources.json @@ -76,6 +76,7 @@ "elasticloadbalancing:ModifyListenerAttributes", "elasticloadbalancing:ModifyLoadBalancerAttributes", "elasticloadbalancing:ModifyRule", + "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes", "elasticloadbalancing:AddListenerCertificates", "elasticloadbalancing:RemoveListenerCertificates", @@ -107,6 +108,7 @@ "rds:ModifyDBInstance", "rds:ModifyDBClusterParameterGroup", "rds:ResetDBClusterParameterGroup", + "rds:DisableHttpEndpoint", "resource-groups:CreateGroup", "resource-groups:DeleteGroup", "route53:ChangeResourceRecordSets", From a4ec95b0f5ffe6f0891ea85711964e9c7852460b Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Wed, 9 Jul 2025 07:26:27 +0100 Subject: [PATCH 18/50] Fix flaky search form test The test tries to create a vaccination record with a different programme to that currently being search across, however because the vaccination record factory picks programmes at random it's possible that sometimes the two programmes will match. There's a unique index on the type of programme meaning when two programmes of the same type are created we get a unique constraint failure. --- spec/forms/search_form_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/forms/search_form_spec.rb b/spec/forms/search_form_spec.rb index 5036a077e3..ea6414575c 100644 --- a/spec/forms/search_form_spec.rb +++ b/spec/forms/search_form_spec.rb @@ -544,7 +544,8 @@ create( :vaccination_record, patient:, - performed_ods_code: organisation.ods_code + performed_ods_code: organisation.ods_code, + programme: create(:programme, :hpv) ) end From 65182af5992851b52b1c4d36b080fcd9294528ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:00:37 +0000 Subject: [PATCH 19/50] Bump faraday from 2.13.1 to 2.13.2 Bumps [faraday](https://github.com/lostisland/faraday) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/lostisland/faraday/releases) - [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md) - [Commits](https://github.com/lostisland/faraday/compare/v2.13.1...v2.13.2) --- updated-dependencies: - dependency-name: faraday dependency-version: 2.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa3f44496e..911170ce9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,13 +231,13 @@ GEM railties (>= 6.1.0) faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.1) + faraday (2.13.2) faraday-net_http (>= 2.0, < 3.5) json logger faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) - faraday-net_http (3.4.0) + faraday-net_http (3.4.1) net-http (>= 0.5.0) ferrum (0.17.1) addressable (~> 2.5) From ae9bcc0bb402a3b5323404312458a5fa25bccd9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:00:39 +0000 Subject: [PATCH 20/50] Bump phonelib from 0.10.9 to 0.10.10 Bumps [phonelib](https://github.com/daddyz/phonelib) from 0.10.9 to 0.10.10. - [Release notes](https://github.com/daddyz/phonelib/releases) - [Changelog](https://github.com/daddyz/phonelib/blob/master/CHANGELOG.md) - [Commits](https://github.com/daddyz/phonelib/compare/v0.10.9...v0.10.10) --- updated-dependencies: - dependency-name: phonelib dependency-version: 0.10.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa3f44496e..9a43b36531 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -429,7 +429,7 @@ GEM ast (~> 2.4.1) racc pg (1.5.9) - phonelib (0.10.9) + phonelib (0.10.10) pp (0.6.2) prettyprint prettier_print (1.2.1) From 03e7e897fff93113afbda60654e625009f3dcd70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:00:53 +0000 Subject: [PATCH 21/50] Bump pagy from 9.3.4 to 9.3.5 Bumps [pagy](https://github.com/ddnexus/pagy) from 9.3.4 to 9.3.5. - [Release notes](https://github.com/ddnexus/pagy/releases) - [Changelog](https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md) - [Commits](https://github.com/ddnexus/pagy/compare/9.3.4...9.3.5) --- updated-dependencies: - dependency-name: pagy dependency-version: 9.3.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa3f44496e..583e1d5b01 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -423,7 +423,7 @@ GEM webfinger (~> 2.0) orm_adapter (0.5.0) ostruct (0.6.2) - pagy (9.3.4) + pagy (9.3.5) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) From 829baf4dca0c22f258f906d67dafd1777326a000 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:00:53 +0000 Subject: [PATCH 22/50] Bump flipper-active_record from 1.3.4 to 1.3.5 Bumps [flipper-active_record](https://github.com/flippercloud/flipper) from 1.3.4 to 1.3.5. - [Release notes](https://github.com/flippercloud/flipper/releases) - [Changelog](https://github.com/flippercloud/flipper/blob/main/Changelog.md) - [Commits](https://github.com/flippercloud/flipper/compare/v1.3.4...v1.3.5) --- updated-dependencies: - dependency-name: flipper-active_record dependency-version: 1.3.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa3f44496e..f6d5a23742 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -254,9 +254,9 @@ GEM nokogiri (>= 1.11.4) flipper (1.3.5) concurrent-ruby (< 2) - flipper-active_record (1.3.4) + flipper-active_record (1.3.5) activerecord (>= 4.2, < 9) - flipper (~> 1.3.4) + flipper (~> 1.3.5) flipper-ui (1.3.5) erubi (>= 1.0.0, < 2.0.0) flipper (~> 1.3.5) From 0760ee7fabb646e101b51427eaa5593b327ec060 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:01:08 +0000 Subject: [PATCH 23/50] Bump aws-sdk-ec2 from 1.533.0 to 1.536.0 Bumps [aws-sdk-ec2](https://github.com/aws/aws-sdk-ruby) from 1.533.0 to 1.536.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-ec2/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) --- updated-dependencies: - dependency-name: aws-sdk-ec2 dependency-version: 1.536.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa3f44496e..f47d425df7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -124,7 +124,7 @@ GEM base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-ec2 (1.533.0) + aws-sdk-ec2 (1.536.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) aws-sdk-ecr (1.104.0) From 7277ed4ea9f12d5e8a02207ab9b9c3fe125e5b8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:01:42 +0000 Subject: [PATCH 24/50] Bump esbuild from 0.25.5 to 0.25.6 Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.5 to 0.25.6. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.25.5...v0.25.6) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.25.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 312 ++++++++++++++++++++++++++------------------------- 2 files changed, 160 insertions(+), 154 deletions(-) diff --git a/package.json b/package.json index e3cb2d9075..645ec61dac 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.16", "accessible-autocomplete": "^3.0.1", - "esbuild": "^0.25.5", + "esbuild": "^0.25.6", "govuk-frontend": "^5.11.0", "idb": "^8.0.3", "nhsuk-frontend": "9.6.1", diff --git a/yarn.lock b/yarn.lock index 726b620d10..4c1fc81f47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1423,130 +1423,135 @@ dependencies: tslib "^2.4.0" -"@esbuild/aix-ppc64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" - integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== - -"@esbuild/android-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" - integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== - -"@esbuild/android-arm@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" - integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== - -"@esbuild/android-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" - integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== - -"@esbuild/darwin-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34" - integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== - -"@esbuild/darwin-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" - integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== - -"@esbuild/freebsd-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" - integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== - -"@esbuild/freebsd-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" - integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== - -"@esbuild/linux-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" - integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== - -"@esbuild/linux-arm@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" - integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== - -"@esbuild/linux-ia32@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" - integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== - -"@esbuild/linux-loong64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" - integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== - -"@esbuild/linux-mips64el@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" - integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== - -"@esbuild/linux-ppc64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" - integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== - -"@esbuild/linux-riscv64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" - integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== - -"@esbuild/linux-s390x@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" - integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== - -"@esbuild/linux-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" - integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== - -"@esbuild/netbsd-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" - integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== - -"@esbuild/netbsd-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" - integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== - -"@esbuild/openbsd-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" - integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== - -"@esbuild/openbsd-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" - integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== - -"@esbuild/sunos-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" - integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== - -"@esbuild/win32-arm64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" - integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== - -"@esbuild/win32-ia32@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" - integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== - -"@esbuild/win32-x64@0.25.5": - version "0.25.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" - integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== +"@esbuild/aix-ppc64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz#164b19122e2ed54f85469df9dea98ddb01d5e79e" + integrity sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw== + +"@esbuild/android-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz#8f539e7def848f764f6432598e51cc3820fde3a5" + integrity sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA== + +"@esbuild/android-arm@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.6.tgz#4ceb0f40113e9861169be83e2a670c260dd234ff" + integrity sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg== + +"@esbuild/android-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.6.tgz#ad4f280057622c25fe985c08999443a195dc63a8" + integrity sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A== + +"@esbuild/darwin-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz#d1f04027396b3d6afc96bacd0d13167dfd9f01f7" + integrity sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA== + +"@esbuild/darwin-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz#2b4a6cedb799f635758d7832d75b23772c8ef68f" + integrity sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg== + +"@esbuild/freebsd-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz#a26266cc97dd78dc3c3f3d6788b1b83697b1055d" + integrity sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg== + +"@esbuild/freebsd-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz#9feb8e826735c568ebfd94859b22a3fbb6a9bdd2" + integrity sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ== + +"@esbuild/linux-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz#c07cbed8e249f4c28e7f32781d36fc4695293d28" + integrity sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ== + +"@esbuild/linux-arm@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz#d6e2cd8ef3196468065d41f13fa2a61aaa72644a" + integrity sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw== + +"@esbuild/linux-ia32@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz#3e682bd47c4eddcc4b8f1393dfc8222482f17997" + integrity sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw== + +"@esbuild/linux-loong64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz#473f5ea2e52399c08ad4cd6b12e6dbcddd630f05" + integrity sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg== + +"@esbuild/linux-mips64el@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz#9960631c9fd61605b0939c19043acf4ef2b51718" + integrity sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw== + +"@esbuild/linux-ppc64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz#477cbf8bb04aa034b94f362c32c86b5c31db8d3e" + integrity sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw== + +"@esbuild/linux-riscv64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz#bcdb46c8fb8e93aa779e9a0a62cd4ac00dcac626" + integrity sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w== + +"@esbuild/linux-s390x@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz#f412cf5fdf0aea849ff51c73fd817c6c0234d46d" + integrity sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw== + +"@esbuild/linux-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz#d8233c09b5ebc0c855712dc5eeb835a3a3341108" + integrity sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig== + +"@esbuild/netbsd-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz#f51ae8dd1474172e73cf9cbaf8a38d1c72dd8f1a" + integrity sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q== + +"@esbuild/netbsd-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz#a267538602c0e50a858cf41dcfe5d8036f8da8e7" + integrity sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g== + +"@esbuild/openbsd-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz#a51be60c425b85c216479b8c344ad0511635f2d2" + integrity sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg== + +"@esbuild/openbsd-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz#7e4a743c73f75562e29223ba69d0be6c9c9008da" + integrity sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw== + +"@esbuild/openharmony-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz#2087a5028f387879154ebf44bdedfafa17682e5b" + integrity sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA== + +"@esbuild/sunos-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz#56531f861723ea0dc6283a2bb8837304223cb736" + integrity sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA== + +"@esbuild/win32-arm64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz#f4989f033deac6fae323acff58764fa8bc01436e" + integrity sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q== + +"@esbuild/win32-ia32@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz#b260e9df71e3939eb33925076d39f63cec7d1525" + integrity sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ== + +"@esbuild/win32-x64@0.25.6": + version "0.25.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz#4276edd5c105bc28b11c6a1f76fb9d29d1bd25c1" + integrity sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA== "@hotwired/stimulus-webpack-helpers@^1.0.0": version "1.0.1" @@ -3366,36 +3371,37 @@ esbuild-jest@^0.5.0: "@babel/plugin-transform-modules-commonjs" "^7.12.13" babel-jest "^26.6.3" -esbuild@^0.25.5: - version "0.25.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430" - integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ== +esbuild@^0.25.6: + version "0.25.6" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.6.tgz#9b82a3db2fa131aec069ab040fd57ed0a880cdcd" + integrity sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.5" - "@esbuild/android-arm" "0.25.5" - "@esbuild/android-arm64" "0.25.5" - "@esbuild/android-x64" "0.25.5" - "@esbuild/darwin-arm64" "0.25.5" - "@esbuild/darwin-x64" "0.25.5" - "@esbuild/freebsd-arm64" "0.25.5" - "@esbuild/freebsd-x64" "0.25.5" - "@esbuild/linux-arm" "0.25.5" - "@esbuild/linux-arm64" "0.25.5" - "@esbuild/linux-ia32" "0.25.5" - "@esbuild/linux-loong64" "0.25.5" - "@esbuild/linux-mips64el" "0.25.5" - "@esbuild/linux-ppc64" "0.25.5" - "@esbuild/linux-riscv64" "0.25.5" - "@esbuild/linux-s390x" "0.25.5" - "@esbuild/linux-x64" "0.25.5" - "@esbuild/netbsd-arm64" "0.25.5" - "@esbuild/netbsd-x64" "0.25.5" - "@esbuild/openbsd-arm64" "0.25.5" - "@esbuild/openbsd-x64" "0.25.5" - "@esbuild/sunos-x64" "0.25.5" - "@esbuild/win32-arm64" "0.25.5" - "@esbuild/win32-ia32" "0.25.5" - "@esbuild/win32-x64" "0.25.5" + "@esbuild/aix-ppc64" "0.25.6" + "@esbuild/android-arm" "0.25.6" + "@esbuild/android-arm64" "0.25.6" + "@esbuild/android-x64" "0.25.6" + "@esbuild/darwin-arm64" "0.25.6" + "@esbuild/darwin-x64" "0.25.6" + "@esbuild/freebsd-arm64" "0.25.6" + "@esbuild/freebsd-x64" "0.25.6" + "@esbuild/linux-arm" "0.25.6" + "@esbuild/linux-arm64" "0.25.6" + "@esbuild/linux-ia32" "0.25.6" + "@esbuild/linux-loong64" "0.25.6" + "@esbuild/linux-mips64el" "0.25.6" + "@esbuild/linux-ppc64" "0.25.6" + "@esbuild/linux-riscv64" "0.25.6" + "@esbuild/linux-s390x" "0.25.6" + "@esbuild/linux-x64" "0.25.6" + "@esbuild/netbsd-arm64" "0.25.6" + "@esbuild/netbsd-x64" "0.25.6" + "@esbuild/openbsd-arm64" "0.25.6" + "@esbuild/openbsd-x64" "0.25.6" + "@esbuild/openharmony-arm64" "0.25.6" + "@esbuild/sunos-x64" "0.25.6" + "@esbuild/win32-arm64" "0.25.6" + "@esbuild/win32-ia32" "0.25.6" + "@esbuild/win32-x64" "0.25.6" escalade@^3.1.1: version "3.1.1" From fca17b7f4e3caf618d4178e077560dc8f64f0e44 Mon Sep 17 00:00:00 2001 From: Moritz Bogs Date: Mon, 7 Jul 2025 14:30:47 +0100 Subject: [PATCH 25/50] Improve data replication deployment * Add the options to deploy just the db snapshot or just the webapp * Taint the DB cluster to force recreation if necessary * Simplify workflow by removing Destroy option that was never used anyway and by doing the db recreation in a single step --- .../workflows/data-replication-pipeline.yml | 132 +++++++----------- terraform/data_replication/rds.tf | 12 +- 2 files changed, 61 insertions(+), 83 deletions(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 4b6325b003..0140b92965 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -1,5 +1,5 @@ name: Data replication pipeline -run-name: ${{ inputs.action }} data replication resources for ${{ inputs.environment }} +run-name: ${{ inputs.deployment_type }} for data replication resources for ${{ inputs.environment }} on: workflow_dispatch: @@ -15,18 +15,17 @@ on: - qa - sandbox-alpha - sandbox-beta + deployment_type: + description: Deployment type + required: true + type: choice + options: + - Deployment with DB recreation + - Application only deployment image_tag: description: Docker image tag to deploy required: false type: string - action: - description: Action to perform on data replication env - required: true - type: choice - options: - - Destroy - - Recreate - default: Recreate db_snapshot_arn: description: ARN of the DB snapshot to use (optional) required: false @@ -50,8 +49,8 @@ concurrency: group: deploy-data-replica-${{ inputs.environment }} jobs: - prepare: - if: ${{ inputs.action == 'Recreate' }} + prepare-db-replica: + if: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} name: Prepare data replica runs-on: ubuntu-latest permissions: @@ -95,58 +94,13 @@ jobs: terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade DB_SECRET_ARN=$(terraform output --raw db_secret_arn) echo "DB_SECRET_ARN=$DB_SECRET_ARN" >> $GITHUB_OUTPUT - - name: ECR login - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - name: Get docker image digest - id: get-docker-image-digest - run: | - set -e - DOCKER_IMAGE="${{ steps.login-ecr.outputs.registry }}/mavis/webapp:${{ inputs.image_tag || github.sha }}" - docker pull "$DOCKER_IMAGE" - DOCKER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$DOCKER_IMAGE") - DIGEST="${DOCKER_DIGEST#*@}" - echo "DIGEST=$DIGEST" >> $GITHUB_OUTPUT outputs: SNAPSHOT_ARN: ${{ steps.get-latest-snapshot.outputs.SNAPSHOT_ARN }} DB_SECRET_ARN: ${{ steps.get-db-secret-arn.outputs.DB_SECRET_ARN }} - DOCKER_DIGEST: ${{ steps.get-docker-image-digest.outputs.DIGEST }} - - plan-destroy: - name: Plan destruction job - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ env.aws_role }} - aws-region: eu-west-2 - - name: Install terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.11.4 - - name: Terraform Plan - run: | - set -e - terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - terraform plan -destroy -var-file="env/${{ inputs.environment }}.tfvars" -var="image_digest=filler_value" \ - -var="db_secret_arn=filler_value" -var="imported_snapshot=filler_value" \ - -out ${{ runner.temp }}/tfplan_destroy | tee ${{ runner.temp }}/tf_stdout - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: tfplan_destroy_infrastructure-${{ inputs.environment }} - path: ${{ runner.temp }}/tfplan_destroy - destroy: - name: Destroy data replication infrastructure + prepare-webapp: + name: Prepare webapp runs-on: ubuntu-latest - needs: plan-destroy - environment: ${{ inputs.environment }} permissions: id-token: write steps: @@ -157,32 +111,35 @@ jobs: with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - - name: Install terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.11.4 - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: tfplan_destroy_infrastructure-${{ inputs.environment }} - path: ${{ runner.temp }} - - name: Terraform Destroy - id: destroy + - name: ECR login + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + - name: Get docker image digest + id: get-docker-image-digest run: | set -e - terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - terraform apply ${{ runner.temp }}/tfplan_destroy + DOCKER_IMAGE="${{ steps.login-ecr.outputs.registry }}/mavis/webapp:${{ inputs.image_tag || github.sha }}" + docker pull "$DOCKER_IMAGE" + DOCKER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$DOCKER_IMAGE") + DIGEST="${DOCKER_DIGEST#*@}" + echo "DIGEST=$DIGEST" >> $GITHUB_OUTPUT + outputs: + DOCKER_DIGEST: ${{ steps.get-docker-image-digest.outputs.DIGEST }} plan: name: Terraform plan runs-on: ubuntu-latest needs: - - prepare - - destroy + - prepare-db-replica + - prepare-webapp + if: ${{ !cancelled() && + (needs.prepare-db-replica.result == 'success' || needs.prepare-db-replica.result == 'skipped') && + needs.prepare-webapp.result == 'success' }} env: - SNAPSHOT_ARN: ${{ needs.prepare.outputs.SNAPSHOT_ARN }} - DB_SECRET_ARN: ${{ needs.prepare.outputs.DB_SECRET_ARN }} - DOCKER_DIGEST: ${{ needs.prepare.outputs.DOCKER_DIGEST }} + SNAPSHOT_ARN: ${{ needs.prepare-db-replica.outputs.SNAPSHOT_ARN }} + DB_SECRET_ARN: ${{ needs.prepare-db-replica.outputs.DB_SECRET_ARN || 'arn:aws:secretsmanager:eu-west-2:000000000000:secret:placeholder' }} + DOCKER_DIGEST: ${{ needs.prepare-webapp.outputs.DOCKER_DIGEST }} + REPLACE_DB_CLUSTER: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} permissions: id-token: write steps: @@ -200,12 +157,24 @@ jobs: - name: Terraform Plan id: plan run: | - set -e + set -eo pipefail terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - terraform plan -var="image_digest=${{ env.DOCKER_DIGEST }}" -var="db_secret_arn=${{ env.DB_SECRET_ARN }}" \ - -var="imported_snapshot=${{ env.SNAPSHOT_ARN }}" -var-file="env/${{ inputs.environment }}.tfvars" \ - -var='allowed_egress_cidr_blocks=${{ inputs.egress_cidr }}' \ - -out ${{ runner.temp }}/tfplan | tee ${{ runner.temp }}/tf_stdout + + CIDR_BLOCKS='${{ inputs.egress_cidr }}' + PLAN_ARGS=( + "plan" + "-var=image_digest=${{ env.DOCKER_DIGEST }}" + "-var=db_secret_arn=${{ env.DB_SECRET_ARN }}" + "-var=imported_snapshot=${{ env.SNAPSHOT_ARN }}" + "-var-file=env/${{ inputs.environment }}.tfvars" + "-var=allowed_egress_cidr_blocks=$CIDR_BLOCKS" + "-out=${{ runner.temp }}/tfplan" + ) + + if [ "${{ env.REPLACE_DB_CLUSTER }}" = "true" ]; then + PLAN_ARGS+=("-replace" "aws_rds_cluster.cluster") + fi + terraform "${PLAN_ARGS[@]}" | tee ${{ runner.temp }}/tf_stdout - name: Upload artifact uses: actions/upload-artifact@v4 with: @@ -216,6 +185,7 @@ jobs: name: Terraform apply runs-on: ubuntu-latest needs: plan + if: ${{ !cancelled() && needs.plan.result == 'success' }} environment: ${{ inputs.environment }} permissions: id-token: write diff --git a/terraform/data_replication/rds.tf b/terraform/data_replication/rds.tf index 455fe3673c..0c1515c318 100644 --- a/terraform/data_replication/rds.tf +++ b/terraform/data_replication/rds.tf @@ -17,7 +17,7 @@ resource "aws_security_group_rule" "rds_inbound" { } resource "aws_rds_cluster" "cluster" { - cluster_identifier = "${local.name_prefix}-rds" + cluster_identifier = "${local.name_prefix}-rds-${formatdate("hh-mm-ss", timestamp())}" engine = "aurora-postgresql" engine_mode = "provisioned" database_name = "manage_vaccinations" @@ -34,14 +34,22 @@ resource "aws_rds_cluster" "cluster" { max_capacity = var.max_aurora_capacity_units min_capacity = 0.5 } + + lifecycle { + ignore_changes = [cluster_identifier] + } } resource "aws_rds_cluster_instance" "instance" { cluster_identifier = aws_rds_cluster.cluster.id - identifier = "${local.name_prefix}-rds-instance" + identifier = "${local.name_prefix}-rds-instance-${formatdate("hh-mm-ss", timestamp())}" instance_class = "db.serverless" engine = aws_rds_cluster.cluster.engine engine_version = aws_rds_cluster.cluster.engine_version db_subnet_group_name = aws_db_subnet_group.dbsg.name promotion_tier = 1 + + lifecycle { + ignore_changes = [identifier] + } } From 60198f981ac37fee4f215f1458deb3f271ada67c Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Wed, 9 Jul 2025 11:32:25 +0100 Subject: [PATCH 26/50] Add Session#eligible_programmes_for This adds a new method on the session class which can be used to determine the eligible programmes for a particular patient. This logic already exists in the service but is duplicates in a number of places, instead we can avoid that duplication and have the logic in a single method. --- app/models/note.rb | 4 +--- app/models/patient_session.rb | 4 +--- app/models/session.rb | 5 +++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 64be471ea2..54c632bdfe 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -33,9 +33,7 @@ class Note < ApplicationRecord validates :body, presence: true, length: { maximum: 1000 } - def programmes - session.programmes.select { it.year_groups.include?(year_group) } - end + def programmes = session.eligible_programmes_for(year_group:) private diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb index f23934e074..9c996c78ef 100644 --- a/app/models/patient_session.rb +++ b/app/models/patient_session.rb @@ -201,9 +201,7 @@ def can_record_as_already_vaccinated?(programme:) !session.today? && patient.vaccination_status(programme:).none_yet? end - def programmes - session.programmes.select { it.year_groups.include?(patient.year_group) } - end + def programmes = session.eligible_programmes_for(patient:) def session_status(programme:) session_statuses.find { it.programme_id == programme.id } || diff --git a/app/models/session.rb b/app/models/session.rb index 0596df4f3f..676ac1dd1a 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -164,6 +164,11 @@ def vaccine_methods programmes.flat_map(&:vaccine_methods).uniq.sort end + def eligible_programmes_for(patient: nil, year_group: nil) + year_group ||= patient.year_group + programmes.select { it.year_groups.include?(year_group) } + end + def dates session_dates.map(&:value).compact end From 21d07aecd6c97b31e8725d2acbb6f2b47a316456 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Wed, 9 Jul 2025 11:44:41 +0100 Subject: [PATCH 27/50] Get programmes for GOV.UK Notify personalisation At the moment, to determine the programmes (used in GOV.UK Notify personalisation), they need to be passed in explicitly when we send the email. This is causing an issue with the `vaccine_side_effects` variable where it's not available for session reminders because the programmes cannot be determined correctly. Jira-Issue: MAV-1355 --- app/lib/govuk_notify_personalisation.rb | 4 ++++ spec/lib/govuk_notify_personalisation_spec.rb | 23 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index a6c6106cc7..e19f91fb32 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -30,6 +30,10 @@ def initialize( consent&.organisation || vaccination_record&.organisation @team = session&.team || consent_form&.team || vaccination_record&.team @vaccination_record = vaccination_record + + if @programmes.empty? && @session.present? && @patient.present? + @programmes = @session.eligible_programmes_for(patient: @patient) + end end def to_h diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index 8a50beb325..e3c7dd4512 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true describe GovukNotifyPersonalisation do - subject(:to_h) do - described_class.new( + subject(:to_h) { described_class.new(**params).to_h } + + let(:params) do + { patient:, session:, consent:, consent_form:, programmes:, vaccination_record: - ).to_h + } end let(:programmes) { [create(:programme, :hpv)] } @@ -256,5 +258,20 @@ ) ) end + + context "when programmes comes from the session" do + let(:params) do + { patient:, session:, consent:, consent_form:, vaccination_record: } + end + + it do + expect(to_h).to match( + hash_including( + vaccine_side_effects: + "- generally feeling unwell\n- swelling or pain where the injection was given" + ) + ) + end + end end end From d533c1dc3382b67a8750b86bbcfb078e2b5d45e4 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 25 Jun 2025 22:37:48 +0100 Subject: [PATCH 28/50] Add nhs_immunisations_api_synced_at to ... ... VaccinationRecord Jira-Issue: MAV-1477 --- app/models/vaccination_record.rb | 49 ++++++++++--------- ...ons_api_synced_at_to_vaccination_record.rb | 12 +++++ db/schema.rb | 3 +- spec/factories/vaccination_records.rb | 49 ++++++++++--------- spec/models/vaccination_record_spec.rb | 49 ++++++++++--------- 5 files changed, 89 insertions(+), 73 deletions(-) create mode 100644 db/migrate/20250625212637_add_nhs_immunisations_api_synced_at_to_vaccination_record.rb diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb index 40e1e6c18a..9c73258317 100644 --- a/app/models/vaccination_record.rb +++ b/app/models/vaccination_record.rb @@ -4,30 +4,31 @@ # # Table name: vaccination_records # -# id :bigint not null, primary key -# confirmation_sent_at :datetime -# delivery_method :integer -# delivery_site :integer -# discarded_at :datetime -# dose_sequence :integer -# full_dose :boolean -# location_name :string -# notes :text -# outcome :integer not null -# pending_changes :jsonb not null -# performed_at :datetime not null -# performed_by_family_name :string -# performed_by_given_name :string -# performed_ods_code :string -# uuid :uuid not null -# created_at :datetime not null -# updated_at :datetime not null -# batch_id :bigint -# patient_id :bigint -# performed_by_user_id :bigint -# programme_id :bigint not null -# session_id :bigint -# vaccine_id :bigint +# id :bigint not null, primary key +# confirmation_sent_at :datetime +# delivery_method :integer +# delivery_site :integer +# discarded_at :datetime +# dose_sequence :integer +# full_dose :boolean +# location_name :string +# nhs_immunisations_api_synced_at :datetime +# notes :text +# outcome :integer not null +# pending_changes :jsonb not null +# performed_at :datetime not null +# performed_by_family_name :string +# performed_by_given_name :string +# performed_ods_code :string +# uuid :uuid not null +# created_at :datetime not null +# updated_at :datetime not null +# batch_id :bigint +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # Indexes # diff --git a/db/migrate/20250625212637_add_nhs_immunisations_api_synced_at_to_vaccination_record.rb b/db/migrate/20250625212637_add_nhs_immunisations_api_synced_at_to_vaccination_record.rb new file mode 100644 index 0000000000..50cd46d594 --- /dev/null +++ b/db/migrate/20250625212637_add_nhs_immunisations_api_synced_at_to_vaccination_record.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddNHSImmunisationsAPISyncedAtToVaccinationRecord < ActiveRecord::Migration[ + 8.0 +] + def change + add_column :vaccination_records, + :nhs_immunisations_api_synced_at, + :datetime, + null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 256eb74bd2..7b4a3b6253 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_02_142922) do +ActiveRecord::Schema[8.0].define(version: 2025_07_02_162254) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_trgm" @@ -820,6 +820,7 @@ t.string "performed_ods_code" t.bigint "vaccine_id" t.boolean "full_dose" + t.datetime "nhs_immunisations_api_synced_at" t.index ["batch_id"], name: "index_vaccination_records_on_batch_id" t.index ["discarded_at"], name: "index_vaccination_records_on_discarded_at" t.index ["patient_id"], name: "index_vaccination_records_on_patient_id" diff --git a/spec/factories/vaccination_records.rb b/spec/factories/vaccination_records.rb index ef5e6e37a8..9614573505 100644 --- a/spec/factories/vaccination_records.rb +++ b/spec/factories/vaccination_records.rb @@ -4,30 +4,31 @@ # # Table name: vaccination_records # -# id :bigint not null, primary key -# confirmation_sent_at :datetime -# delivery_method :integer -# delivery_site :integer -# discarded_at :datetime -# dose_sequence :integer -# full_dose :boolean -# location_name :string -# notes :text -# outcome :integer not null -# pending_changes :jsonb not null -# performed_at :datetime not null -# performed_by_family_name :string -# performed_by_given_name :string -# performed_ods_code :string -# uuid :uuid not null -# created_at :datetime not null -# updated_at :datetime not null -# batch_id :bigint -# patient_id :bigint -# performed_by_user_id :bigint -# programme_id :bigint not null -# session_id :bigint -# vaccine_id :bigint +# id :bigint not null, primary key +# confirmation_sent_at :datetime +# delivery_method :integer +# delivery_site :integer +# discarded_at :datetime +# dose_sequence :integer +# full_dose :boolean +# location_name :string +# nhs_immunisations_api_synced_at :datetime +# notes :text +# outcome :integer not null +# pending_changes :jsonb not null +# performed_at :datetime not null +# performed_by_family_name :string +# performed_by_given_name :string +# performed_ods_code :string +# uuid :uuid not null +# created_at :datetime not null +# updated_at :datetime not null +# batch_id :bigint +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # Indexes # diff --git a/spec/models/vaccination_record_spec.rb b/spec/models/vaccination_record_spec.rb index 840fe24078..fc21328598 100644 --- a/spec/models/vaccination_record_spec.rb +++ b/spec/models/vaccination_record_spec.rb @@ -4,30 +4,31 @@ # # Table name: vaccination_records # -# id :bigint not null, primary key -# confirmation_sent_at :datetime -# delivery_method :integer -# delivery_site :integer -# discarded_at :datetime -# dose_sequence :integer -# full_dose :boolean -# location_name :string -# notes :text -# outcome :integer not null -# pending_changes :jsonb not null -# performed_at :datetime not null -# performed_by_family_name :string -# performed_by_given_name :string -# performed_ods_code :string -# uuid :uuid not null -# created_at :datetime not null -# updated_at :datetime not null -# batch_id :bigint -# patient_id :bigint -# performed_by_user_id :bigint -# programme_id :bigint not null -# session_id :bigint -# vaccine_id :bigint +# id :bigint not null, primary key +# confirmation_sent_at :datetime +# delivery_method :integer +# delivery_site :integer +# discarded_at :datetime +# dose_sequence :integer +# full_dose :boolean +# location_name :string +# nhs_immunisations_api_synced_at :datetime +# notes :text +# outcome :integer not null +# pending_changes :jsonb not null +# performed_at :datetime not null +# performed_by_family_name :string +# performed_by_given_name :string +# performed_ods_code :string +# uuid :uuid not null +# created_at :datetime not null +# updated_at :datetime not null +# batch_id :bigint +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # Indexes # From c8a27f5ed964ca10b52319257f419b19833785a4 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 25 Jun 2025 22:39:06 +0100 Subject: [PATCH 29/50] Add SyncVaccinationRecordToNHSEJob This job will be responsible for syncing a Mavis vaccination record with NHS via the Immunisation API. The first task will be to simply create the immunisation record if it hasn't already been synced, implemented here. Funcionality will be added to update or delete the record as necessary to keep the record in sync. Jira-Issue: MAV-1477 --- .../sync_vaccination_record_to_nhs_job.rb | 16 ++++++ ...sync_vaccination_record_to_nhs_job_spec.rb | 49 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/jobs/sync_vaccination_record_to_nhs_job.rb create mode 100644 spec/jobs/sync_vaccination_record_to_nhs_job_spec.rb diff --git a/app/jobs/sync_vaccination_record_to_nhs_job.rb b/app/jobs/sync_vaccination_record_to_nhs_job.rb new file mode 100644 index 0000000000..704ce95573 --- /dev/null +++ b/app/jobs/sync_vaccination_record_to_nhs_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class SyncVaccinationRecordToNHSJob < ApplicationJob + queue_as :immunisation_api + + def perform(vaccination_record) + if vaccination_record.nhs_immunisations_api_synced_at.present? + Rails.logger.info( + "Vaccination record already synced: #{vaccination_record.id}" + ) + return + end + + NHS::ImmunisationsAPI.record_immunisation(vaccination_record) + end +end diff --git a/spec/jobs/sync_vaccination_record_to_nhs_job_spec.rb b/spec/jobs/sync_vaccination_record_to_nhs_job_spec.rb new file mode 100644 index 0000000000..95323f8ebc --- /dev/null +++ b/spec/jobs/sync_vaccination_record_to_nhs_job_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +describe SyncVaccinationRecordToNHSJob, type: :job do + subject(:perform_now) { described_class.perform_now(vaccination_record) } + + before { allow(NHS::ImmunisationsAPI).to receive(:record_immunisation) } + + let(:vaccination_record) do + instance_double( + VaccinationRecord, + id: "123", + nhs_immunisations_api_synced_at: nil + ) + end + + it "sends the vaccination record to the NHS Immunisations API" do + perform_now + + expect(NHS::ImmunisationsAPI).to have_received(:record_immunisation).with( + vaccination_record + ) + end + + context "when the vaccination record has already been synced" do + let(:vaccination_record) do + instance_double( + VaccinationRecord, + id: "123", + nhs_immunisations_api_synced_at: Time.current + ) + end + + it "does not send the vaccination record to the NHS Immunisations API" do + perform_now + + expect(NHS::ImmunisationsAPI).not_to have_received(:record_immunisation) + end + + it "logs that the record has already been synced" do + allow(Rails.logger).to receive(:info) + + perform_now + + expect(Rails.logger).to have_received(:info).with( + "Vaccination record already synced: 123" + ) + end + end +end From bbe24914f2c3b7cb3bd2295abd2f3881920117ab Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 25 Jun 2025 23:13:39 +0100 Subject: [PATCH 30/50] Add mavis vaccination-records sync cli command Jira-Issue: MAV-1477 --- app/lib/mavis_cli.rb | 1 + app/lib/mavis_cli/vaccination_records/sync.rb | 37 ++++++ .../cli_vaccination_records_sync_spec.rb | 121 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 app/lib/mavis_cli/vaccination_records/sync.rb create mode 100644 spec/features/cli_vaccination_records_sync_spec.rb diff --git a/app/lib/mavis_cli.rb b/app/lib/mavis_cli.rb index b38d141c26..094dc29bf5 100644 --- a/app/lib/mavis_cli.rb +++ b/app/lib/mavis_cli.rb @@ -24,3 +24,4 @@ def self.progress_bar(total) require_relative "mavis_cli/gias/check_import" require_relative "mavis_cli/gias/download" require_relative "mavis_cli/gias/import" +require_relative "mavis_cli/vaccination_records/sync" diff --git a/app/lib/mavis_cli/vaccination_records/sync.rb b/app/lib/mavis_cli/vaccination_records/sync.rb new file mode 100644 index 0000000000..4e73779a1a --- /dev/null +++ b/app/lib/mavis_cli/vaccination_records/sync.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module MavisCLI + module VaccinationRecords + class Sync < Dry::CLI::Command + desc "Sync a vaccination record to NHS Immunisations API" + argument :vaccination_record_id, + required: true, + desc: "ID of vaccination record to sync" + + def call(vaccination_record_id:, **) + MavisCLI.load_rails + + vaccination_record = + ::VaccinationRecord.find_by(id: vaccination_record_id) + + if vaccination_record.nil? + puts "Vaccination record with ID #{vaccination_record_id} not found" + return + end + + if vaccination_record.nhs_immunisations_api_synced_at.present? + puts "Vaccination record #{vaccination_record_id} has already been" \ + " synced at #{vaccination_record.nhs_immunisations_api_synced_at}" + return + end + + SyncVaccinationRecordToNHSJob.perform_now(vaccination_record) + puts "Successfully synced vaccination record #{vaccination_record_id}" + end + end + end + + register "vaccination-records" do |prefix| + prefix.register "sync", VaccinationRecords::Sync + end +end diff --git a/spec/features/cli_vaccination_records_sync_spec.rb b/spec/features/cli_vaccination_records_sync_spec.rb new file mode 100644 index 0000000000..2fa7aa74c9 --- /dev/null +++ b/spec/features/cli_vaccination_records_sync_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require_relative "../../app/lib/mavis_cli" + +describe "mavis vaccination-records sync" do + context "when the vaccination record exists and has not been synced" do + it "syncs the vaccination record to the NHS API" do + given_a_vaccination_record_exists + and_the_nhs_api_is_available + when_i_run_the_sync_command + then_the_vaccination_record_is_synced_to_the_immunisations_api + end + end + + context "when the vaccination record does not exist" do + it "displays an error message" do + when_i_run_the_sync_command_with_an_invalid_id + then_an_error_message_is_displayed + end + end + + context "when the vaccination record has already been synced" do + it "displays a message indicating it has already been synced" do + given_a_synced_vaccination_record_exists + when_i_run_the_sync_command_for_synced_record + then_the_already_synced_message_is_displayed + end + end + + private + + def given_a_vaccination_record_exists + organisation = create(:organisation) + programme = create(:programme, type: "hpv") + patient = create(:patient, organisation:) + vaccine = create(:vaccine, :gardasil, programme:) + batch = create(:batch, vaccine:, expiry: "2023-03-20", name: "X8U375AL") + + @vaccination_record = + create( + :vaccination_record, + patient:, + programme:, + vaccine:, + batch:, + nhs_immunisations_api_synced_at: nil + ) + end + + def given_a_synced_vaccination_record_exists + organisation = create(:organisation) + programme = create(:programme, type: "hpv") + patient = create(:patient, organisation:) + @synced_vaccination_record = + create( + :vaccination_record, + patient:, + programme:, + nhs_immunisations_api_synced_at: Time.current + ) + end + + def and_the_nhs_api_is_available + @nhs_api_request = + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).with( + headers: { + "Content-Type" => "application/fhir+json", + "Accept" => "application/fhir+json" + } + ).to_return(status: 200, body: "", headers: {}) + end + + def when_i_run_the_sync_command + @output = + capture_output do + Dry::CLI.new(MavisCLI).call( + arguments: ["vaccination-records", "sync", @vaccination_record.id] + ) + end + end + + def when_i_run_the_sync_command_with_an_invalid_id + @output = + capture_output do + Dry::CLI.new(MavisCLI).call( + arguments: %w[vaccination-records sync 999999] + ) + end + end + + def when_i_run_the_sync_command_for_synced_record + @output = + capture_output do + Dry::CLI.new(MavisCLI).call( + arguments: [ + "vaccination-records", + "sync", + @synced_vaccination_record.id + ] + ) + end + end + + def then_the_vaccination_record_is_synced_to_the_immunisations_api + expect(@nhs_api_request).to have_been_made + expect(@output).to include( + "Successfully synced vaccination record #{@vaccination_record.id}" + ) + end + + def then_an_error_message_is_displayed + expect(@output).to include("Vaccination record with ID 999999 not found") + end + + def then_the_already_synced_message_is_displayed + expect(@output).to include("has already been synced at") + end +end From 3583ab248d8c05408143d52f151f377a8de4049d Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 2 Jul 2025 17:10:39 +0100 Subject: [PATCH 31/50] Simplify error handling in imms api Jira-Issue: MAV-1477 --- app/lib/nhs/immunisations_api.rb | 44 ++++---- spec/lib/nhs/immunisations_api_spec.rb | 143 +++++++++---------------- 2 files changed, 66 insertions(+), 121 deletions(-) diff --git a/app/lib/nhs/immunisations_api.rb b/app/lib/nhs/immunisations_api.rb index 06b0e4cc70..37892ad095 100644 --- a/app/lib/nhs/immunisations_api.rb +++ b/app/lib/nhs/immunisations_api.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true module NHS::ImmunisationsAPI - class PatientNotFound < StandardError - end - class << self def record_immunisation(vaccination_record) NHS::API.connection.post( @@ -11,33 +8,28 @@ def record_immunisation(vaccination_record) vaccination_record.fhir_record.to_json, "Content-Type" => "application/fhir+json" ) - rescue Faraday::Error => e - info = extract_error_info(e.response[:body]) - Rails.logger.error( - "Error recording vaccination record (#{vaccination_record.id}):" \ - " [#{info[:code]}] #{info[:diagnostics]}" - ) - raise e + rescue Faraday::ClientError => e + if (diagnostics = extract_error_diagnostics(e&.response)).present? + raise "Error syncing vaccination #{vaccination_record.id} record to" \ + " Immunisations API: #{diagnostics}" + else + raise + end end - def extract_error_info(response_body) - return { code: nil, diagnostics: "No response body" } unless response_body - - response = JSON.parse(response_body, symbolize_names: true) + private - if response.empty? - { code: nil, diagnostics: "No response body" } - elsif response[:issue].blank? - { code: nil, diagnostics: "No issues in response" } - elsif response[:issue].first[:severity] != "error" - { code: nil, diagnostics: "Issue is not an error" } - else - diagnostics = response[:issue].first[:diagnostics] - if diagnostics.match?(/NHS Number: \d{10} is invalid.*/) - diagnostics.replace("NHS Number is invalid or it doesn't exist") - end + def extract_error_diagnostics(response) + return nil if response.nil? || response[:body].blank? - { code: response[:issue].first[:code], diagnostics: diagnostics } + begin + JSON.parse(response[:body], symbolize_names: true).dig( + :issue, + 0, + :diagnostics + ) + rescue JSON::ParserError + nil end end end diff --git a/spec/lib/nhs/immunisations_api_spec.rb b/spec/lib/nhs/immunisations_api_spec.rb index 7a5d0d3e7c..db6ddf2730 100644 --- a/spec/lib/nhs/immunisations_api_spec.rb +++ b/spec/lib/nhs/immunisations_api_spec.rb @@ -75,113 +75,66 @@ end context "an error is returned by the api" do - let(:response) do - { issue: [{ severity: "error", code:, diagnostics: }] }.to_json - end - - before do - stub_request( - :post, - "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" - ).to_return(status: 400, body: response, headers: {}) - - allow(Rails.logger).to receive(:error).and_return(true) - end + context "4XX error" do + let(:response) do + { + resourceType: "OperationOutcome", + id: "bc2c3c82-4392-4314-9d6b-a7345f82d923", + 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: "NOT-FOUND" + } + ] + }, + diagnostics: "Invalid patient ID" + } + ] + }.to_json + end - context "generic error" do - let(:code) { "invalid" } - let(:diagnostics) { "Invalid patient ID" } + before do + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).to_return(status: 404, body: response, headers: {}) + end - it "raises an error with the correct message" do - begin + it "raises an error with the diagnostic message" do + expect { described_class.record_immunisation(vaccination_record) - rescue StandardError - nil - end - - expect(Rails.logger).to have_received(:error).with( - /\[invalid\] Invalid patient ID/ + }.to raise_error( + StandardError, + "Error syncing vaccination #{vaccination_record.id} record to" \ + " Immunisations API: Invalid patient ID" ) end end - context "the error is invalid NHS number" do - let(:code) { "exception" } - let(:diagnostics) do - "NHS Number: 1234567890 is invalid or it doesn't exist" + context "generic error" do + before do + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).to_return(status: 500, body: nil, headers: {}) end - it "raises an error with the correct message" do - begin + it "raises an error with the diagnostic message" do + expect { described_class.record_immunisation(vaccination_record) - rescue StandardError - nil - end - - expect(Rails.logger).to have_received(:error).with( - /\[exception\] NHS Number is invalid or it doesn't exist/ - ) + }.to raise_error(Faraday::Error) end end end end - - describe "extract_error_info" do - subject(:error_info) { described_class.extract_error_info(response) } - - context "response body has an error" do - let(:response) do - { - issue: [ - { - severity: "error", - code: "invalid", - diagnostics: "Invalid patient ID" - } - ] - }.to_json - end - - its([:code]) { should eq "invalid" } - its([:diagnostics]) { should eq "Invalid patient ID" } - end - - context "when the response body is empty" do - let(:response) { nil } - - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No response body" } - end - - context "when the response body has no issue attribute" do - let(:response) { "{}" } - - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No response body" } - end - - context "when the response body has no issues" do - let(:response) { '{"issues": [] }' } - - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No issues in response" } - end - - context "the issue severity is not 'error'" do - let(:response) do - { - issue: [ - { - severity: "warning", - code: "not-found", - diagnostics: "Patient not found" - } - ] - }.to_json - end - - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "Issue is not an error" } - end - end end From aa26b145c15be0f83234af5b48a0d59f7a182ebc Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Mon, 30 Jun 2025 16:10:33 +0100 Subject: [PATCH 32/50] Add nhs_immunisations_api_id to vaccination record This will be used to store the uuid we get from the NHS Immunisations API. Jira-Issue: MAV-1477 --- app/models/vaccination_record.rb | 18 ++++++++++-------- ...munisations_api_id_to_vaccination_record.rb | 11 +++++++++++ db/schema.rb | 2 ++ spec/factories/vaccination_records.rb | 18 ++++++++++-------- spec/models/vaccination_record_spec.rb | 18 ++++++++++-------- 5 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20250630144421_add_nhs_immunisations_api_id_to_vaccination_record.rb diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb index 9c73258317..8c20a8270a 100644 --- a/app/models/vaccination_record.rb +++ b/app/models/vaccination_record.rb @@ -24,6 +24,7 @@ # created_at :datetime not null # updated_at :datetime not null # batch_id :bigint +# nhs_immunisations_api_id :string # patient_id :bigint # performed_by_user_id :bigint # programme_id :bigint not null @@ -32,14 +33,15 @@ # # Indexes # -# index_vaccination_records_on_batch_id (batch_id) -# index_vaccination_records_on_discarded_at (discarded_at) -# index_vaccination_records_on_patient_id (patient_id) -# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) -# index_vaccination_records_on_programme_id (programme_id) -# index_vaccination_records_on_session_id (session_id) -# index_vaccination_records_on_uuid (uuid) UNIQUE -# index_vaccination_records_on_vaccine_id (vaccine_id) +# index_vaccination_records_on_batch_id (batch_id) +# index_vaccination_records_on_discarded_at (discarded_at) +# index_vaccination_records_on_nhs_immunisations_api_id (nhs_immunisations_api_id) UNIQUE +# index_vaccination_records_on_patient_id (patient_id) +# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) +# index_vaccination_records_on_programme_id (programme_id) +# index_vaccination_records_on_session_id (session_id) +# index_vaccination_records_on_uuid (uuid) UNIQUE +# index_vaccination_records_on_vaccine_id (vaccine_id) # # Foreign Keys # diff --git a/db/migrate/20250630144421_add_nhs_immunisations_api_id_to_vaccination_record.rb b/db/migrate/20250630144421_add_nhs_immunisations_api_id_to_vaccination_record.rb new file mode 100644 index 0000000000..fc69c5e874 --- /dev/null +++ b/db/migrate/20250630144421_add_nhs_immunisations_api_id_to_vaccination_record.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddNHSImmunisationsAPIIdToVaccinationRecord < ActiveRecord::Migration[8.0] + def change + add_column :vaccination_records, + :nhs_immunisations_api_id, + :string, + null: true + add_index :vaccination_records, :nhs_immunisations_api_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7b4a3b6253..1ccadc57f1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -821,8 +821,10 @@ t.bigint "vaccine_id" t.boolean "full_dose" t.datetime "nhs_immunisations_api_synced_at" + t.string "nhs_immunisations_api_id" t.index ["batch_id"], name: "index_vaccination_records_on_batch_id" t.index ["discarded_at"], name: "index_vaccination_records_on_discarded_at" + t.index ["nhs_immunisations_api_id"], name: "index_vaccination_records_on_nhs_immunisations_api_id", unique: true t.index ["patient_id"], name: "index_vaccination_records_on_patient_id" t.index ["performed_by_user_id"], name: "index_vaccination_records_on_performed_by_user_id" t.index ["programme_id"], name: "index_vaccination_records_on_programme_id" diff --git a/spec/factories/vaccination_records.rb b/spec/factories/vaccination_records.rb index 9614573505..3d4850cb80 100644 --- a/spec/factories/vaccination_records.rb +++ b/spec/factories/vaccination_records.rb @@ -24,6 +24,7 @@ # created_at :datetime not null # updated_at :datetime not null # batch_id :bigint +# nhs_immunisations_api_id :string # patient_id :bigint # performed_by_user_id :bigint # programme_id :bigint not null @@ -32,14 +33,15 @@ # # Indexes # -# index_vaccination_records_on_batch_id (batch_id) -# index_vaccination_records_on_discarded_at (discarded_at) -# index_vaccination_records_on_patient_id (patient_id) -# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) -# index_vaccination_records_on_programme_id (programme_id) -# index_vaccination_records_on_session_id (session_id) -# index_vaccination_records_on_uuid (uuid) UNIQUE -# index_vaccination_records_on_vaccine_id (vaccine_id) +# index_vaccination_records_on_batch_id (batch_id) +# index_vaccination_records_on_discarded_at (discarded_at) +# index_vaccination_records_on_nhs_immunisations_api_id (nhs_immunisations_api_id) UNIQUE +# index_vaccination_records_on_patient_id (patient_id) +# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) +# index_vaccination_records_on_programme_id (programme_id) +# index_vaccination_records_on_session_id (session_id) +# index_vaccination_records_on_uuid (uuid) UNIQUE +# index_vaccination_records_on_vaccine_id (vaccine_id) # # Foreign Keys # diff --git a/spec/models/vaccination_record_spec.rb b/spec/models/vaccination_record_spec.rb index fc21328598..d4930393f9 100644 --- a/spec/models/vaccination_record_spec.rb +++ b/spec/models/vaccination_record_spec.rb @@ -24,6 +24,7 @@ # created_at :datetime not null # updated_at :datetime not null # batch_id :bigint +# nhs_immunisations_api_id :string # patient_id :bigint # performed_by_user_id :bigint # programme_id :bigint not null @@ -32,14 +33,15 @@ # # Indexes # -# index_vaccination_records_on_batch_id (batch_id) -# index_vaccination_records_on_discarded_at (discarded_at) -# index_vaccination_records_on_patient_id (patient_id) -# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) -# index_vaccination_records_on_programme_id (programme_id) -# index_vaccination_records_on_session_id (session_id) -# index_vaccination_records_on_uuid (uuid) UNIQUE -# index_vaccination_records_on_vaccine_id (vaccine_id) +# index_vaccination_records_on_batch_id (batch_id) +# index_vaccination_records_on_discarded_at (discarded_at) +# index_vaccination_records_on_nhs_immunisations_api_id (nhs_immunisations_api_id) UNIQUE +# index_vaccination_records_on_patient_id (patient_id) +# index_vaccination_records_on_performed_by_user_id (performed_by_user_id) +# index_vaccination_records_on_programme_id (programme_id) +# index_vaccination_records_on_session_id (session_id) +# index_vaccination_records_on_uuid (uuid) UNIQUE +# index_vaccination_records_on_vaccine_id (vaccine_id) # # Foreign Keys # From 119557d0b6eeff0c487a9fcc85244b5b7b4cca8f Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 9 Jul 2025 13:58:29 +0100 Subject: [PATCH 33/50] Add nhs_immunisations_api_etag to vaccinations These will be used when updating or deleting later. Jira-Issue: MAV-1477 --- app/models/vaccination_record.rb | 1 + ...s_immunisations_api_etag_to_vaccination_record.rb | 12 ++++++++++++ db/schema.rb | 1 + spec/factories/vaccination_records.rb | 1 + spec/models/vaccination_record_spec.rb | 1 + 5 files changed, 16 insertions(+) create mode 100644 db/migrate/20250630140421_add_nhs_immunisations_api_etag_to_vaccination_record.rb diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb index 8c20a8270a..34d44f272f 100644 --- a/app/models/vaccination_record.rb +++ b/app/models/vaccination_record.rb @@ -12,6 +12,7 @@ # dose_sequence :integer # full_dose :boolean # location_name :string +# nhs_immunisations_api_etag :string # nhs_immunisations_api_synced_at :datetime # notes :text # outcome :integer not null diff --git a/db/migrate/20250630140421_add_nhs_immunisations_api_etag_to_vaccination_record.rb b/db/migrate/20250630140421_add_nhs_immunisations_api_etag_to_vaccination_record.rb new file mode 100644 index 0000000000..7acf3681ec --- /dev/null +++ b/db/migrate/20250630140421_add_nhs_immunisations_api_etag_to_vaccination_record.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddNHSImmunisationsAPIEtagToVaccinationRecord < ActiveRecord::Migration[ + 8.0 +] + def change + add_column :vaccination_records, + :nhs_immunisations_api_etag, + :string, + null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ccadc57f1..cfe10734c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -822,6 +822,7 @@ t.boolean "full_dose" t.datetime "nhs_immunisations_api_synced_at" t.string "nhs_immunisations_api_id" + t.string "nhs_immunisations_api_etag" t.index ["batch_id"], name: "index_vaccination_records_on_batch_id" t.index ["discarded_at"], name: "index_vaccination_records_on_discarded_at" t.index ["nhs_immunisations_api_id"], name: "index_vaccination_records_on_nhs_immunisations_api_id", unique: true diff --git a/spec/factories/vaccination_records.rb b/spec/factories/vaccination_records.rb index 3d4850cb80..3681852ea9 100644 --- a/spec/factories/vaccination_records.rb +++ b/spec/factories/vaccination_records.rb @@ -12,6 +12,7 @@ # dose_sequence :integer # full_dose :boolean # location_name :string +# nhs_immunisations_api_etag :string # nhs_immunisations_api_synced_at :datetime # notes :text # outcome :integer not null diff --git a/spec/models/vaccination_record_spec.rb b/spec/models/vaccination_record_spec.rb index d4930393f9..b63cfe8f66 100644 --- a/spec/models/vaccination_record_spec.rb +++ b/spec/models/vaccination_record_spec.rb @@ -12,6 +12,7 @@ # dose_sequence :integer # full_dose :boolean # location_name :string +# nhs_immunisations_api_etag :string # nhs_immunisations_api_synced_at :datetime # notes :text # outcome :integer not null From b1a9976eec558cc4b2dc553227f3817ccda19f91 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Wed, 9 Jul 2025 14:08:46 +0100 Subject: [PATCH 34/50] Record info when syncing vaccination record This info will be used for performing updates and deletes later. Jira-Issue: MAV-1477 --- app/lib/nhs/immunisations_api.rb | 34 ++++- .../cli_vaccination_records_sync_spec.rb | 9 +- spec/lib/nhs/immunisations_api_spec.rb | 144 +++++++++++++----- 3 files changed, 140 insertions(+), 47 deletions(-) diff --git a/app/lib/nhs/immunisations_api.rb b/app/lib/nhs/immunisations_api.rb index 37892ad095..8f525ae144 100644 --- a/app/lib/nhs/immunisations_api.rb +++ b/app/lib/nhs/immunisations_api.rb @@ -3,11 +3,27 @@ module NHS::ImmunisationsAPI class << self def record_immunisation(vaccination_record) - NHS::API.connection.post( - "/immunisation-fhir-api/FHIR/R4/Immunization", - vaccination_record.fhir_record.to_json, - "Content-Type" => "application/fhir+json" - ) + response = + NHS::API.connection.post( + "/immunisation-fhir-api/FHIR/R4/Immunization", + vaccination_record.fhir_record.to_json, + "Content-Type" => "application/fhir+json" + ) + + if response.status == 201 + vaccination_record.update!( + nhs_immunisations_api_id: + extract_nhs_id(response.headers.fetch("location")), + nhs_immunisations_api_synced_at: Time.current, + # We would normally retrieve this from the API response, but the NHS + # Immunisations API does not return this to us, yet. + nhs_immunisations_api_etag: 1 + ) + else + raise "Error syncing vaccination record #{vaccination_record.id} to" \ + " Immunisations API: unexpected response status" \ + " #{response.status}" + end rescue Faraday::ClientError => e if (diagnostics = extract_error_diagnostics(e&.response)).present? raise "Error syncing vaccination #{vaccination_record.id} record to" \ @@ -32,5 +48,13 @@ def extract_error_diagnostics(response) nil end end + + def extract_nhs_id(location) + if (match = location.match(%r{Immunization/([a-f0-9-]+)})) + match[1] + else + raise UnrecognisedLocation, location + end + end end end diff --git a/spec/features/cli_vaccination_records_sync_spec.rb b/spec/features/cli_vaccination_records_sync_spec.rb index 2fa7aa74c9..a9c944c883 100644 --- a/spec/features/cli_vaccination_records_sync_spec.rb +++ b/spec/features/cli_vaccination_records_sync_spec.rb @@ -70,7 +70,14 @@ def and_the_nhs_api_is_available "Content-Type" => "application/fhir+json", "Accept" => "application/fhir+json" } - ).to_return(status: 200, body: "", headers: {}) + ).to_return( + status: 201, + body: "", + headers: { + location: + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/11112222-3333-4444-5555-666677778888" + } + ) end def when_i_run_the_sync_command diff --git a/spec/lib/nhs/immunisations_api_spec.rb b/spec/lib/nhs/immunisations_api_spec.rb index db6ddf2730..066fc7163b 100644 --- a/spec/lib/nhs/immunisations_api_spec.rb +++ b/spec/lib/nhs/immunisations_api_spec.rb @@ -49,6 +49,20 @@ end describe "record_immunisation" do + before do + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).to_return( + status: 201, + body: "", + headers: { + location: + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/ffff1111-eeee-2222-dddd-3333eeee4444" + } + ) + end + it "sends the correct JSON payload" do expected_body = File.read(Rails.root.join("spec/fixtures/fhir/immunisation.json")).chomp @@ -56,59 +70,107 @@ # stree-ignore stubbed_request = stub_request( - :post, - "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + :post, "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" ) .with { |request| - expect(request.headers["Accept"]).to eq "application/fhir+json" - expect( - request.headers["Content-Type"] - ).to eq "application/fhir+json" - expect(request.body).to eq expected_body - true - } - .to_return(status: 200, body: "", headers: {}) + expect(request.headers["Accept"]).to eq "application/fhir+json" + expect( + request.headers["Content-Type"] + ).to eq "application/fhir+json" + expect(request.body).to eq expected_body + true + } + .to_return(status: 201, + body: "", + headers: { + location: + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/ffff1111-eeee-2222-dddd-3333eeee4444" + }) described_class.record_immunisation(vaccination_record) expect(stubbed_request).to have_been_made end + it "stores the id from the response" do + described_class.record_immunisation(vaccination_record) + + expect( + vaccination_record.nhs_immunisations_api_id + ).to eq "ffff1111-eeee-2222-dddd-3333eeee4444" + end + + it "stores the nhs_immunisations_api_synced_at from the response" do + freeze_time do + described_class.record_immunisation(vaccination_record) + + expect( + vaccination_record.nhs_immunisations_api_synced_at + ).to eq Time.current + end + end + + it "initialises the etag to 1" do + described_class.record_immunisation(vaccination_record) + + expect(vaccination_record.nhs_immunisations_api_etag).to eq "1" + end + context "an error is returned by the api" do - context "4XX error" do - let(:response) do - { - resourceType: "OperationOutcome", - id: "bc2c3c82-4392-4314-9d6b-a7345f82d923", - 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: "NOT-FOUND" - } - ] - }, - diagnostics: "Invalid patient ID" - } + before do + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).to_return(status: status, body: response, headers: {}) + end + + let(:status) { 201 } + let(:code) { nil } + let(:diagnostics) { nil } + + let(:response) do + { + resourceType: "OperationOutcome", + id: "bc2c3c82-4392-4314-9d6b-a7345f82d923", + meta: { + profile: [ + "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome" ] - }.to_json - end + }, + issue: [ + { + severity: "error", + code: "invalid", + details: { + coding: [ + { + system: "https://fhir.nhs.uk/Codesystem/http-error-codes", + code: + } + ] + }, + diagnostics: + } + ] + }.to_json + end - before do - stub_request( - :post, - "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" - ).to_return(status: 404, body: response, headers: {}) + context "unexpected response status" do + let(:status) { 200 } + let(:response) { "" } + + it "raises an error saying the response is unexpected" do + expect { + described_class.record_immunisation(vaccination_record) + }.to raise_error( + "Error syncing vaccination record #{vaccination_record.id} to" \ + " Immunisations API: unexpected response status 200" + ) end + end + + context "4XX error" do + let(:status) { 404 } it "raises an error with the diagnostic message" do expect { From fc991b27c507d3b366b94a17f16993274533d9a7 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Thu, 3 Jul 2025 15:20:20 +0100 Subject: [PATCH 35/50] Add feature flag for immunisations fhir api This flag is designed to control any and all connectivity to the Immunisations FHIR API. Jira-Issue: MAV-1477 --- app/lib/nhs/immunisations_api.rb | 10 ++++- config/feature_flags.yml | 3 ++ .../cli_vaccination_records_sync_spec.rb | 2 + spec/lib/nhs/immunisations_api_spec.rb | 40 ++++++++++++------- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/app/lib/nhs/immunisations_api.rb b/app/lib/nhs/immunisations_api.rb index 8f525ae144..76b0f95751 100644 --- a/app/lib/nhs/immunisations_api.rb +++ b/app/lib/nhs/immunisations_api.rb @@ -3,6 +3,14 @@ module NHS::ImmunisationsAPI class << self def record_immunisation(vaccination_record) + unless Flipper.enabled?(:immunisations_fhir_api_integration) + Rails.logger.info( + "Not syncing vaccination record to immunisations API as the feature" \ + " flag is disabled: #{vaccination_record.id}" + ) + return + end + response = NHS::API.connection.post( "/immunisation-fhir-api/FHIR/R4/Immunization", @@ -26,7 +34,7 @@ def record_immunisation(vaccination_record) end rescue Faraday::ClientError => e if (diagnostics = extract_error_diagnostics(e&.response)).present? - raise "Error syncing vaccination #{vaccination_record.id} record to" \ + raise "Error syncing vaccination record #{vaccination_record.id} to" \ " Immunisations API: #{diagnostics}" else raise diff --git a/config/feature_flags.yml b/config/feature_flags.yml index 4d4c33f6a5..ea7534bf76 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -8,3 +8,6 @@ dev_tools: Developer tools useful for testing and debugging. mesh_jobs: Export vaccination records to MESH automatically. offline_working: Prototype support for using Mavis offline. + +immunisations_fhir_api_integration: Master switch to control communications with + NHS Immunistaions FHIR API. diff --git a/spec/features/cli_vaccination_records_sync_spec.rb b/spec/features/cli_vaccination_records_sync_spec.rb index a9c944c883..9cdbba047d 100644 --- a/spec/features/cli_vaccination_records_sync_spec.rb +++ b/spec/features/cli_vaccination_records_sync_spec.rb @@ -78,6 +78,8 @@ def and_the_nhs_api_is_available "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/11112222-3333-4444-5555-666677778888" } ) + + Flipper.enable :immunisations_fhir_api_integration end def when_i_run_the_sync_command diff --git a/spec/lib/nhs/immunisations_api_spec.rb b/spec/lib/nhs/immunisations_api_spec.rb index 066fc7163b..7ccfe6a579 100644 --- a/spec/lib/nhs/immunisations_api_spec.rb +++ b/spec/lib/nhs/immunisations_api_spec.rb @@ -47,21 +47,22 @@ created_at: Time.zone.parse("2021-02-07T13:28:17.271+00:00") ) end + let!(:stubbed_request) do + stub_request( + :post, + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" + ).to_return( + status: 201, + body: "", + headers: { + location: + "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/ffff1111-eeee-2222-dddd-3333eeee4444" + } + ) + end describe "record_immunisation" do - before do - stub_request( - :post, - "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4/Immunization" - ).to_return( - status: 201, - body: "", - headers: { - location: - "https://sandbox.api.service.nhs.uk/immunisation-fhir-api/Immunization/ffff1111-eeee-2222-dddd-3333eeee4444" - } - ) - end + before { Flipper.enable(:immunisations_fhir_api_integration) } it "sends the correct JSON payload" do expected_body = @@ -171,13 +172,14 @@ context "4XX error" do let(:status) { 404 } + let(:diagnostics) { "Invalid patient ID" } it "raises an error with the diagnostic message" do expect { described_class.record_immunisation(vaccination_record) }.to raise_error( StandardError, - "Error syncing vaccination #{vaccination_record.id} record to" \ + "Error syncing vaccination record #{vaccination_record.id} to" \ " Immunisations API: Invalid patient ID" ) end @@ -198,5 +200,15 @@ end end end + + context "the immunisations_fhir_api_integration feature flag is disabled" do + before { Flipper.disable(:immunisations_fhir_api_integration) } + + it "does not make a request to the NHS API" do + described_class.record_immunisation(vaccination_record) + + expect(stubbed_request).not_to have_been_made + end + end end end From e2b7d21bce1445d224e95ff8e2ed17bb9be476c2 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Fri, 4 Jul 2025 19:30:47 +0100 Subject: [PATCH 36/50] Set default dose sequence for flu to 1 Jira-Issue: MAV-1502 --- app/models/programme.rb | 2 +- spec/lib/reports/offline_session_exporter_spec.rb | 2 +- spec/models/immunisation_import_row_spec.rb | 3 ++- spec/models/programme_spec.rb | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/programme.rb b/app/models/programme.rb index e3a8e60014..3f6edfd940 100644 --- a/app/models/programme.rb +++ b/app/models/programme.rb @@ -103,7 +103,7 @@ def vaccinated_dose_sequence end def default_dose_sequence - hpv? ? vaccinated_dose_sequence : nil + hpv? || flu? ? vaccinated_dose_sequence : nil end def maximum_dose_sequence diff --git a/spec/lib/reports/offline_session_exporter_spec.rb b/spec/lib/reports/offline_session_exporter_spec.rb index 539d966f5e..5d7167b134 100644 --- a/spec/lib/reports/offline_session_exporter_spec.rb +++ b/spec/lib/reports/offline_session_exporter_spec.rb @@ -955,7 +955,7 @@ def validation_formula(worksheet:, column_name:, row: 1) context "Flu programme" do let(:programme) { create(:programme, :flu) } let(:expected_programme) { "Flu" } - let(:expected_dose_sequence) { nil } + let(:expected_dose_sequence) { 1 } include_examples "generates a report" end diff --git a/spec/models/immunisation_import_row_spec.rb b/spec/models/immunisation_import_row_spec.rb index a527814dac..a649623a5b 100644 --- a/spec/models/immunisation_import_row_spec.rb +++ b/spec/models/immunisation_import_row_spec.rb @@ -1179,7 +1179,8 @@ "DATE_OF_VACCINATION" => session.dates.first.strftime("%Y%m%d"), "SESSION_ID" => session.id.to_s, "ORGANISATION_CODE" => organisation.ods_code, - "PERFORMING_PROFESSIONAL_EMAIL" => create(:user).email + "PERFORMING_PROFESSIONAL_EMAIL" => create(:user).email, + "DOSE_SEQUENCE" => "1" ) end diff --git a/spec/models/programme_spec.rb b/spec/models/programme_spec.rb index 2cdc7eda42..2a057f0fe7 100644 --- a/spec/models/programme_spec.rb +++ b/spec/models/programme_spec.rb @@ -160,7 +160,7 @@ context "with a Flu programme" do let(:programme) { build(:programme, :flu) } - it { should be_nil } + it { should eq(1) } end context "with an HPV programme" do From f7bc93ce2acc5930c564009570f4dda8cee6b283 Mon Sep 17 00:00:00 2001 From: Mike Thompson Date: Tue, 8 Jul 2025 17:43:06 +0100 Subject: [PATCH 37/50] Show dose number against Flu vaccination records Jira-Issue: MAV-1502 --- app/components/app_vaccination_record_summary_component.rb | 2 -- .../app_vaccination_record_summary_component_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/components/app_vaccination_record_summary_component.rb b/app/components/app_vaccination_record_summary_component.rb index 0a441708a9..5aa6bff8c4 100644 --- a/app/components/app_vaccination_record_summary_component.rb +++ b/app/components/app_vaccination_record_summary_component.rb @@ -329,8 +329,6 @@ def dose_number_value end def dose_number - return nil if @programme.seasonal? - dose_sequence = @vaccination_record.dose_sequence if dose_sequence.nil? diff --git a/spec/components/app_vaccination_record_summary_component_spec.rb b/spec/components/app_vaccination_record_summary_component_spec.rb index 1435fecdb0..00800c5ff8 100644 --- a/spec/components/app_vaccination_record_summary_component_spec.rb +++ b/spec/components/app_vaccination_record_summary_component_spec.rb @@ -151,9 +151,9 @@ let(:programme) { create(:programme, :flu) } it do - expect(rendered).not_to have_css( + expect(rendered).to have_css( ".nhsuk-summary-list__row", - text: "Dose number" + text: "Dose number\nFirst" ) end end From d3d40e6c95e934079091aca3492c2ff008680bca Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Sun, 6 Jul 2025 23:55:02 +0100 Subject: [PATCH 38/50] Add feature flag for sending vaccination records This will be used when recording vaccinations records either through the UI or via upload of offline recording. Jira-Issue: MAV-1482 --- config/feature_flags.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/feature_flags.yml b/config/feature_flags.yml index ea7534bf76..91ff963aa1 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -11,3 +11,6 @@ offline_working: Prototype support for using Mavis offline. immunisations_fhir_api_integration: Master switch to control communications with NHS Immunistaions FHIR API. + +sync_vaccination_records_to_nhs_on_create: Send new vaccinations recorded by + nurses to NHS Immunisations FHIR API. From 8a1b9bafa4ea69c13750533d4240a016c62a474a Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Mon, 7 Jul 2025 16:17:26 +0100 Subject: [PATCH 39/50] Add EnqueueSyncVaccinationRecordToNHSE Encapsulates logic for whether to enqueue a vaccination record to sync it to NHSE or not. Jira-Issue: MAV-1482 --- .../enqueue_sync_vaccination_record_to_nhs.rb | 11 ++++ ...eue_sync_vaccination_record_to_nhs_spec.rb | 61 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 app/lib/enqueue_sync_vaccination_record_to_nhs.rb create mode 100644 spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb diff --git a/app/lib/enqueue_sync_vaccination_record_to_nhs.rb b/app/lib/enqueue_sync_vaccination_record_to_nhs.rb new file mode 100644 index 0000000000..2951739796 --- /dev/null +++ b/app/lib/enqueue_sync_vaccination_record_to_nhs.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EnqueueSyncVaccinationRecordToNHS + def self.call(vaccination_record) + if Flipper.enabled?(:sync_vaccination_records_to_nhs_on_create) && + vaccination_record.programme.type.in?(%w[flu hpv]) && + vaccination_record.administered? + SyncVaccinationRecordToNHSJob.perform_later(vaccination_record) + end + end +end diff --git a/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb b/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb new file mode 100644 index 0000000000..9261a3740c --- /dev/null +++ b/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +describe EnqueueSyncVaccinationRecordToNHS do + context "when the feature flag is disabled" do + before { Flipper.disable(:sync_vaccination_records_to_nhs_on_create) } + + let(:vaccination_record) { create(:vaccination_record) } + + it "does not enqueue the job" do + expect { + described_class.call(vaccination_record) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob) + end + end + + context "when the feature flag is enabled" do + before { Flipper.enable(:sync_vaccination_records_to_nhs_on_create) } + + let(:vaccination_record) do + create(:vaccination_record, outcome:, programme:) + end + let(:outcome) { "administered" } + let(:programme) { create(:programme, type: "flu") } + + context "when the vaccination record is eligible for syncing" do + it "enqueues the job" do + expect { + described_class.call(vaccination_record) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob) + end + end + + VaccinationRecord.defined_enums["outcome"].each_key do |outcome| + next if outcome == "administered" + + context "when the vaccination record outcome is #{outcome}" do + let(:outcome) { outcome } + + it "does not enqueue the job" do + expect { + described_class.call(vaccination_record) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob) + end + end + end + + Programme.defined_enums["type"].each_key do |programme_type| + next if programme_type.in? %w[flu hpv] + + context "when the programme type is #{programme_type}" do + let(:programme) { create(:programme, type: programme_type) } + + it "does not enqueue the job" do + expect { + described_class.call(vaccination_record) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob) + end + end + end + end +end From b39ec21ab7ee9bd8c763c93825d7e53e0d289379 Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Sun, 6 Jul 2025 23:58:29 +0100 Subject: [PATCH 40/50] Send vaccination records to NHSE on recording ... from the UI. Jira-Issue: MAV-1482 --- .../draft_vaccination_records_controller.rb | 2 ++ spec/features/flu_vaccination_administered_spec.rb | 10 ++++++++++ spec/features/hpv_vaccination_administered_spec.rb | 10 ++++++++++ spec/features/menacwy_vaccination_administered_spec.rb | 10 ++++++++++ spec/features/td_ipv_vaccination_administered_spec.rb | 10 ++++++++++ 5 files changed, 42 insertions(+) diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb index 56c896bafb..c69feecced 100644 --- a/app/controllers/draft_vaccination_records_controller.rb +++ b/app/controllers/draft_vaccination_records_controller.rb @@ -120,6 +120,8 @@ def handle_confirm send_vaccination_confirmation(@vaccination_record) if should_notify_parents + EnqueueSyncVaccinationRecordToNHSE.call(@vaccination_record) + # In case the user navigates back to try and edit the newly created # vaccination record. @draft_vaccination_record.update!(editing_id: @vaccination_record.id) diff --git a/spec/features/flu_vaccination_administered_spec.rb b/spec/features/flu_vaccination_administered_spec.rb index 57cfc8532c..c043957fa4 100644 --- a/spec/features/flu_vaccination_administered_spec.rb +++ b/spec/features/flu_vaccination_administered_spec.rb @@ -7,11 +7,13 @@ given_i_am_signed_in_with_flu_programme and_there_is_a_flu_session_today_with_two_patients_ready_to_vaccinate and_there_are_nasal_and_injection_batches + and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled when_i_go_to_the_nasal_only_patient and_i_record_that_the_patient_has_been_vaccinated_with_nasal_spray then_i_see_the_check_and_confirm_page_for_nasal_spray and_i_get_confirmation_after_recording + and_the_vaccination_record_is_synced_to_nhse end scenario "Administered with injection" do @@ -91,6 +93,10 @@ def and_there_are_nasal_and_injection_batches ) end + def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + end + def when_i_go_to_the_nasal_only_patient visit session_record_path(@session) @patient = @nasal_patient @@ -170,4 +176,8 @@ def and_i_pick_a_batch_for_injection choose @injection_batch.name click_button "Continue" end + + def and_the_vaccination_record_is_synced_to_nhse + assert_enqueued_with(job: SyncVaccinationRecordToNHSEJob) + end end diff --git a/spec/features/hpv_vaccination_administered_spec.rb b/spec/features/hpv_vaccination_administered_spec.rb index bbad97e0f3..a8e1ceecdc 100644 --- a/spec/features/hpv_vaccination_administered_spec.rb +++ b/spec/features/hpv_vaccination_administered_spec.rb @@ -5,6 +5,7 @@ scenario "Administered with common delivery site" do given_i_am_signed_in + and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_fill_in_pre_screening_questions @@ -41,6 +42,7 @@ then_i_see_a_success_message and_i_can_no_longer_vaccinate_the_patient and_i_no_longer_see_the_patient_in_the_record_tab + and_the_vaccination_record_is_synced_to_nhse when_i_go_back and_i_save_changes @@ -104,6 +106,10 @@ def given_i_am_signed_in sign_in organisation.users.first end + def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + end + def when_i_go_to_a_patient_that_is_ready_to_vaccinate visit session_record_path(@session) click_link @patient.full_name @@ -242,4 +248,8 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination :vaccination_administered_hpv ) end + + def and_the_vaccination_record_is_synced_to_nhse + assert_enqueued_with(job: SyncVaccinationRecordToNHSEJob) + end end diff --git a/spec/features/menacwy_vaccination_administered_spec.rb b/spec/features/menacwy_vaccination_administered_spec.rb index 8ec0704c8f..60e60f8fe3 100644 --- a/spec/features/menacwy_vaccination_administered_spec.rb +++ b/spec/features/menacwy_vaccination_administered_spec.rb @@ -5,6 +5,7 @@ scenario "Administered" do given_i_am_signed_in + and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_record_that_the_patient_has_been_vaccinated @@ -37,6 +38,7 @@ when_i_confirm_the_details then_i_see_a_success_message and_i_no_longer_see_the_patient_in_the_record_tab + and_the_vaccination_record_is_not_synced_to_nhse when_i_go_back and_i_save_changes @@ -82,6 +84,10 @@ def given_i_am_signed_in sign_in organisation.users.first end + def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + end + def when_i_go_to_a_patient_that_is_ready_to_vaccinate visit session_record_path(@session) click_link @patient.full_name @@ -200,4 +206,8 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination :vaccination_administered_menacwy ) end + + def and_the_vaccination_record_is_not_synced_to_nhse + assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSEJob) + end end diff --git a/spec/features/td_ipv_vaccination_administered_spec.rb b/spec/features/td_ipv_vaccination_administered_spec.rb index 900d67a4c8..8475382cd0 100644 --- a/spec/features/td_ipv_vaccination_administered_spec.rb +++ b/spec/features/td_ipv_vaccination_administered_spec.rb @@ -5,6 +5,7 @@ scenario "Administered" do given_i_am_signed_in + and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_record_that_the_patient_has_been_vaccinated @@ -37,6 +38,7 @@ when_i_confirm_the_details then_i_see_a_success_message and_i_no_longer_see_the_patient_in_the_record_tab + and_the_vaccination_record_is_not_synced_to_nhse when_i_go_back and_i_save_changes @@ -82,6 +84,10 @@ def given_i_am_signed_in sign_in organisation.users.first end + def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + end + def when_i_go_to_a_patient_that_is_ready_to_vaccinate visit session_record_path(@session) click_link @patient.full_name @@ -200,4 +206,8 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination :vaccination_administered_td_ipv ) end + + def and_the_vaccination_record_is_not_synced_to_nhse + assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSEJob) + end end From b4186b9b04464ddbd5caa02b98ee57bd282d12ee Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Mon, 7 Jul 2025 16:19:52 +0100 Subject: [PATCH 41/50] Send vaccinations to NHS on offline import Jira-Issue: MAV-1482 --- .../draft_vaccination_records_controller.rb | 2 +- .../enqueue_sync_vaccination_record_to_nhs.rb | 23 +++- app/models/immunisation_import.rb | 2 + .../flu_vaccination_administered_spec.rb | 12 +- .../hpv_vaccination_administered_spec.rb | 12 +- .../menacwy_vaccination_administered_spec.rb | 12 +- .../td_ipv_vaccination_administered_spec.rb | 12 +- ...eue_sync_vaccination_record_to_nhs_spec.rb | 75 ++++++++++- spec/models/immunisation_import_spec.rb | 120 ++++++++++++++++++ 9 files changed, 237 insertions(+), 33 deletions(-) diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb index c69feecced..f99150c655 100644 --- a/app/controllers/draft_vaccination_records_controller.rb +++ b/app/controllers/draft_vaccination_records_controller.rb @@ -120,7 +120,7 @@ def handle_confirm send_vaccination_confirmation(@vaccination_record) if should_notify_parents - EnqueueSyncVaccinationRecordToNHSE.call(@vaccination_record) + EnqueueSyncVaccinationRecordToNHS.call(@vaccination_record) # In case the user navigates back to try and edit the newly created # vaccination record. diff --git a/app/lib/enqueue_sync_vaccination_record_to_nhs.rb b/app/lib/enqueue_sync_vaccination_record_to_nhs.rb index 2951739796..878e77a8da 100644 --- a/app/lib/enqueue_sync_vaccination_record_to_nhs.rb +++ b/app/lib/enqueue_sync_vaccination_record_to_nhs.rb @@ -1,10 +1,27 @@ # frozen_string_literal: true module EnqueueSyncVaccinationRecordToNHS + PROGRAMME_TYPES = %w[flu hpv].freeze + def self.call(vaccination_record) - if Flipper.enabled?(:sync_vaccination_records_to_nhs_on_create) && - vaccination_record.programme.type.in?(%w[flu hpv]) && - vaccination_record.administered? + return unless Flipper.enabled?(:sync_vaccination_records_to_nhs_on_create) + + vaccination_records = + if vaccination_record.respond_to?(:klass) + vaccination_record + .recorded_in_service + .administered + .where(programmes: { type: PROGRAMME_TYPES }) + .includes(:programme) + elsif vaccination_record.programme.type.in?(PROGRAMME_TYPES) && + vaccination_record.administered? && + vaccination_record.recorded_in_service? + Array(vaccination_record) + else + return + end + + vaccination_records.each do |vaccination_record| SyncVaccinationRecordToNHSJob.perform_later(vaccination_record) end end diff --git a/app/models/immunisation_import.rb b/app/models/immunisation_import.rb index 0bf3a510cd..f28a110e3c 100644 --- a/app/models/immunisation_import.rb +++ b/app/models/immunisation_import.rb @@ -109,5 +109,7 @@ def count_column(vaccination_record) def postprocess_rows! StatusUpdater.call(patient: patients) + + EnqueueSyncVaccinationRecordToNHS.call(vaccination_records) end end diff --git a/spec/features/flu_vaccination_administered_spec.rb b/spec/features/flu_vaccination_administered_spec.rb index c043957fa4..cb15ecc2dc 100644 --- a/spec/features/flu_vaccination_administered_spec.rb +++ b/spec/features/flu_vaccination_administered_spec.rb @@ -7,13 +7,13 @@ given_i_am_signed_in_with_flu_programme and_there_is_a_flu_session_today_with_two_patients_ready_to_vaccinate and_there_are_nasal_and_injection_batches - and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled when_i_go_to_the_nasal_only_patient and_i_record_that_the_patient_has_been_vaccinated_with_nasal_spray then_i_see_the_check_and_confirm_page_for_nasal_spray and_i_get_confirmation_after_recording - and_the_vaccination_record_is_synced_to_nhse + and_the_vaccination_record_is_synced_to_nhs end scenario "Administered with injection" do @@ -93,8 +93,8 @@ def and_there_are_nasal_and_injection_batches ) end - def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled - Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + def and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhs_on_create) end def when_i_go_to_the_nasal_only_patient @@ -177,7 +177,7 @@ def and_i_pick_a_batch_for_injection click_button "Continue" end - def and_the_vaccination_record_is_synced_to_nhse - assert_enqueued_with(job: SyncVaccinationRecordToNHSEJob) + def and_the_vaccination_record_is_synced_to_nhs + assert_enqueued_with(job: SyncVaccinationRecordToNHSJob) end end diff --git a/spec/features/hpv_vaccination_administered_spec.rb b/spec/features/hpv_vaccination_administered_spec.rb index a8e1ceecdc..9604cad838 100644 --- a/spec/features/hpv_vaccination_administered_spec.rb +++ b/spec/features/hpv_vaccination_administered_spec.rb @@ -5,7 +5,7 @@ scenario "Administered with common delivery site" do given_i_am_signed_in - and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_fill_in_pre_screening_questions @@ -42,7 +42,7 @@ then_i_see_a_success_message and_i_can_no_longer_vaccinate_the_patient and_i_no_longer_see_the_patient_in_the_record_tab - and_the_vaccination_record_is_synced_to_nhse + and_the_vaccination_record_is_synced_to_nhs when_i_go_back and_i_save_changes @@ -106,8 +106,8 @@ def given_i_am_signed_in sign_in organisation.users.first end - def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled - Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + def and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhs_on_create) end def when_i_go_to_a_patient_that_is_ready_to_vaccinate @@ -249,7 +249,7 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination ) end - def and_the_vaccination_record_is_synced_to_nhse - assert_enqueued_with(job: SyncVaccinationRecordToNHSEJob) + def and_the_vaccination_record_is_synced_to_nhs + assert_enqueued_with(job: SyncVaccinationRecordToNHSJob) end end diff --git a/spec/features/menacwy_vaccination_administered_spec.rb b/spec/features/menacwy_vaccination_administered_spec.rb index 60e60f8fe3..33261acde9 100644 --- a/spec/features/menacwy_vaccination_administered_spec.rb +++ b/spec/features/menacwy_vaccination_administered_spec.rb @@ -5,7 +5,7 @@ scenario "Administered" do given_i_am_signed_in - and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_record_that_the_patient_has_been_vaccinated @@ -38,7 +38,7 @@ when_i_confirm_the_details then_i_see_a_success_message and_i_no_longer_see_the_patient_in_the_record_tab - and_the_vaccination_record_is_not_synced_to_nhse + and_the_vaccination_record_is_not_synced_to_nhs when_i_go_back and_i_save_changes @@ -84,8 +84,8 @@ def given_i_am_signed_in sign_in organisation.users.first end - def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled - Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + def and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhs_on_create) end def when_i_go_to_a_patient_that_is_ready_to_vaccinate @@ -207,7 +207,7 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination ) end - def and_the_vaccination_record_is_not_synced_to_nhse - assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSEJob) + def and_the_vaccination_record_is_not_synced_to_nhs + assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSJob) end end diff --git a/spec/features/td_ipv_vaccination_administered_spec.rb b/spec/features/td_ipv_vaccination_administered_spec.rb index 8475382cd0..02dfc52a25 100644 --- a/spec/features/td_ipv_vaccination_administered_spec.rb +++ b/spec/features/td_ipv_vaccination_administered_spec.rb @@ -5,7 +5,7 @@ scenario "Administered" do given_i_am_signed_in - and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled + and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled when_i_go_to_a_patient_that_is_ready_to_vaccinate and_i_record_that_the_patient_has_been_vaccinated @@ -38,7 +38,7 @@ when_i_confirm_the_details then_i_see_a_success_message and_i_no_longer_see_the_patient_in_the_record_tab - and_the_vaccination_record_is_not_synced_to_nhse + and_the_vaccination_record_is_not_synced_to_nhs when_i_go_back and_i_save_changes @@ -84,8 +84,8 @@ def given_i_am_signed_in sign_in organisation.users.first end - def and_sync_vaccination_records_to_nhse_on_create_feature_is_enabled - Flipper.enable(:sync_vaccination_records_to_nhse_on_create) + def and_sync_vaccination_records_to_nhs_on_create_feature_is_enabled + Flipper.enable(:sync_vaccination_records_to_nhs_on_create) end def when_i_go_to_a_patient_that_is_ready_to_vaccinate @@ -207,7 +207,7 @@ def and_a_text_is_sent_to_the_parent_confirming_the_vaccination ) end - def and_the_vaccination_record_is_not_synced_to_nhse - assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSEJob) + def and_the_vaccination_record_is_not_synced_to_nhs + assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSJob) end end diff --git a/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb b/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb index 9261a3740c..9614aae391 100644 --- a/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb +++ b/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb @@ -16,14 +16,15 @@ context "when the feature flag is enabled" do before { Flipper.enable(:sync_vaccination_records_to_nhs_on_create) } - let(:vaccination_record) do - create(:vaccination_record, outcome:, programme:) - end let(:outcome) { "administered" } let(:programme) { create(:programme, type: "flu") } + let(:session) { create(:session, programmes: [programme]) } + let(:vaccination_record) do + create(:vaccination_record, outcome:, programme:, session:) + end - context "when the vaccination record is eligible for syncing" do - it "enqueues the job" do + context "with a single vaccination record" do + it "enqueues the job if the vaccination record is elligible to sync" do expect { described_class.call(vaccination_record) }.to have_enqueued_job(SyncVaccinationRecordToNHSJob) @@ -57,5 +58,69 @@ end end end + + context "with a vaccinaton record relation" do + # The strategy is to create a vaccination record for each of the various + # variations, and test that only the correct ones are allowed through + + before do + # Generate historic vaccination record (no session) + create(:vaccination_record, outcome:, programme:) + + # Generate vaccination records for all programme types + Programme.defined_enums["type"].each_key do |programme_type| + next if programme_type == "flu" + programme = create(:programme, type: programme_type) + create(:vaccination_record, outcome: "refused", session:, programme:) + end + + # Generate vaccination records for all outcomes + VaccinationRecord.defined_enums["outcome"].each_key do |outcome| + next if outcome == "administered" + create(:vaccination_record, outcome:, session:, programme:) + end + end + + let(:flu_programme) { Programme.flu.first || create(:programme, :flu) } + let(:hpv_programme) { Programme.hpv.first || create(:programme, :hpv) } + let!(:flu_vaccination_record) do + create( + :vaccination_record, + programme: flu_programme, + session:, + outcome: :administered + ) + end + let!(:hpv_vaccination_record) do + create( + :vaccination_record, + programme: hpv_programme, + session:, + outcome: :administered + ) + end + + it "enqueues the job for each eligible vaccination record" do + expect { + described_class.call(VaccinationRecord.all) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob).exactly(2).times + end + + it "enqueues the eligible flu job" do + expect { + described_class.call(VaccinationRecord.all) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + flu_vaccination_record + ) + end + + it "enqueues the eligible hpv job" do + expect { + described_class.call(VaccinationRecord.all) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + hpv_vaccination_record + ) + end + end end end diff --git a/spec/models/immunisation_import_spec.rb b/spec/models/immunisation_import_spec.rb index f090a5f343..33dcc0b0a7 100644 --- a/spec/models/immunisation_import_spec.rb +++ b/spec/models/immunisation_import_spec.rb @@ -323,4 +323,124 @@ end end end + + describe "#postprocess_row!" do + subject(:immunisation_import) do + create( + :immunisation_import, + organisation:, + vaccination_records: [ + flu_record, + hpv_record, + td_ipv_record, + menacwy_record + ] + ) + end + + before { Flipper.enable :sync_vaccination_records_to_nhs_on_create } + + let(:hpv_programme) { Programme.hpv.first || create(:programme, :hpv) } + let(:flu_programme) { Programme.flu.first || create(:programme, :flu) } + let(:menacwy_programme) { create(:programme, :menacwy) } + let(:td_ipv_programme) { create(:programme, :td_ipv) } + let(:organisation) do + create( + :organisation, + :with_generic_clinic, + ods_code: "R1L", + programmes: [ + hpv_programme, + flu_programme, + menacwy_programme, + td_ipv_programme + ] + ) + end + let(:session) do + create( + :session, + programmes: [ + hpv_programme, + flu_programme, + menacwy_programme, + td_ipv_programme + ] + ) + end + + let(:flu_record) do + create(:vaccination_record, programme: flu_programme, session:) + end + let(:hpv_record) do + create(:vaccination_record, programme: hpv_programme, session:) + end + let(:td_ipv_record) do + create(:vaccination_record, programme: td_ipv_programme, session:) + end + let(:menacwy_record) do + create(:vaccination_record, programme: menacwy_programme, session:) + end + let(:historic_flu_record) do + create(:vaccination_record, programme: flu_programme) + end + let(:refused_hpv_record) do + create( + :vaccination_record, + programme: hpv_programme, + session:, + outcome: "refused" + ) + end + + it "syncs the flu vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob) + .with(flu_record) + .once + .on_queue(:immunisation_api) + end + + it "syncs the hpv vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.to have_enqueued_job(SyncVaccinationRecordToNHSJob) + .with(hpv_record) + .once + .on_queue(:immunisation_api) + end + + it "does not sync the menacwy vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + menacwy_record + ) + end + + it "does not sync the td_ipv vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + td_ipv_record + ) + end + + it "does not sync the historic flu vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + historic_flu_record + ) + end + + it "does not sync the refused hpv vaccination record to the NHS Immunisations API" do + expect { + immunisation_import.send(:postprocess_rows!) + }.not_to have_enqueued_job(SyncVaccinationRecordToNHSJob).with( + refused_hpv_record + ) + end + end end From fe6e088a8e8ee96b4312ec680f2d20b7b0d14429 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:59:31 +0000 Subject: [PATCH 42/50] Bump rubocop-govuk from 5.1.15 to 5.1.16 Bumps [rubocop-govuk](https://github.com/alphagov/rubocop-govuk) from 5.1.15 to 5.1.16. - [Changelog](https://github.com/alphagov/rubocop-govuk/blob/main/CHANGELOG.md) - [Commits](https://github.com/alphagov/rubocop-govuk/compare/v5.1.15...v5.1.16) --- updated-dependencies: - dependency-name: rubocop-govuk dependency-version: 5.1.16 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1a03c35186..587c4e9889 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -552,7 +552,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.4) - rubocop (1.76.2) + rubocop (1.77.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -569,8 +569,8 @@ GEM rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-govuk (5.1.15) - rubocop (= 1.76.2) + rubocop-govuk (5.1.16) + rubocop (= 1.77.0) rubocop-ast (= 1.45.1) rubocop-capybara (= 2.22.1) rubocop-rails (= 2.32.0) From 064df1791e4b3f9357665667b1b9dedd4c3d7ebf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:00:29 +0000 Subject: [PATCH 43/50] Bump aws-sdk-ec2 from 1.536.0 to 1.537.0 Bumps [aws-sdk-ec2](https://github.com/aws/aws-sdk-ruby) from 1.536.0 to 1.537.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-ec2/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) --- updated-dependencies: - dependency-name: aws-sdk-ec2 dependency-version: 1.537.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1a03c35186..ebcc30275f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,7 +113,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1125.0) + aws-partitions (1.1126.0) aws-sdk-accessanalyzer (1.73.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) @@ -124,7 +124,7 @@ GEM base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-ec2 (1.536.0) + aws-sdk-ec2 (1.537.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) aws-sdk-ecr (1.104.0) From 652e1eb6bdfba1efea63386a21dea96f561df652 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 10 Jul 2025 09:10:39 +0100 Subject: [PATCH 44/50] Revert "Get programmes for GOV.UK Notify personalisation" This reverts commit 21d07aecd6c97b31e8725d2acbb6f2b47a316456. Instead this is going to be implemented by passing in an explicit list of programmes that the patient is eligible for. --- app/lib/govuk_notify_personalisation.rb | 4 ---- spec/lib/govuk_notify_personalisation_spec.rb | 23 +++---------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index e19f91fb32..a6c6106cc7 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -30,10 +30,6 @@ def initialize( consent&.organisation || vaccination_record&.organisation @team = session&.team || consent_form&.team || vaccination_record&.team @vaccination_record = vaccination_record - - if @programmes.empty? && @session.present? && @patient.present? - @programmes = @session.eligible_programmes_for(patient: @patient) - end end def to_h diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index e3c7dd4512..8a50beb325 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -1,17 +1,15 @@ # frozen_string_literal: true describe GovukNotifyPersonalisation do - subject(:to_h) { described_class.new(**params).to_h } - - let(:params) do - { + subject(:to_h) do + described_class.new( patient:, session:, consent:, consent_form:, programmes:, vaccination_record: - } + ).to_h end let(:programmes) { [create(:programme, :hpv)] } @@ -258,20 +256,5 @@ ) ) end - - context "when programmes comes from the session" do - let(:params) do - { patient:, session:, consent:, consent_form:, vaccination_record: } - end - - it do - expect(to_h).to match( - hash_including( - vaccine_side_effects: - "- generally feeling unwell\n- swelling or pain where the injection was given" - ) - ) - end - end end end From 361bf252560c2e1e4e20b3fceb1b5c36dfb6e344 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 10 Jul 2025 09:21:44 +0100 Subject: [PATCH 45/50] Limit programmes to those that are safe to vaccinate When sending out the session reminder emails to parents, we should only include information (vaccine side effects) about programmes that the patient is going to be vaccinated for, specifically ignoring those where the patient has already been vaccinated or those that the parent didn't give consent for. Jira-Issue: MAV-1355 --- app/models/session_notification.rb | 17 +++++- spec/models/session_notification_spec.rb | 75 ++++++++++++++++++++---- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/app/models/session_notification.rb b/app/models/session_notification.rb index 7cf51aa891..ab4bddb992 100644 --- a/app/models/session_notification.rb +++ b/app/models/session_notification.rb @@ -82,8 +82,23 @@ def self.create_and_send!( sent_by: current_user ) + programmes = + if type == :school_reminder + patient_session.programmes.select do |programme| + patient.consent_given_and_safe_to_vaccinate?(programme:) + end + else + patient_session.programmes + end + parents.each do |parent| - params = { parent:, patient:, session:, sent_by: current_user } + params = { + parent:, + patient:, + programmes:, + session:, + sent_by: current_user + } EmailDeliveryJob.perform_later(:"session_#{type}", **params) diff --git a/spec/models/session_notification_spec.rb b/spec/models/session_notification_spec.rb index 2c05e9d445..1869379918 100644 --- a/spec/models/session_notification_spec.rb +++ b/spec/models/session_notification_spec.rb @@ -42,11 +42,10 @@ let(:parents) { create_list(:parent, 2) } let(:patient) { create(:patient, parents:, year_group: 10) } let(:programme) { create(:programme, :td_ipv) } - let(:organisation) { create(:organisation, programmes: [programme]) } + let(:programmes) { [programme] } + let(:organisation) { create(:organisation, programmes:) } let(:location) { create(:school, organisation:) } - let(:session) do - create(:session, location:, programmes: [programme], organisation:) - end + let(:session) { create(:session, location:, programmes:, organisation:) } let(:session_date) { session.dates.min } let(:patient_session) { create(:patient_session, patient:, session:) } let(:current_user) { create(:user) } @@ -58,7 +57,10 @@ let(:parent) { parents.first } - before { create(:consent, :given, patient:, parent:, programme:) } + before do + create(:consent, :given, patient:, parent:, programme:) + create(:patient_consent_status, :given, patient:, programme:) + end it "creates a record" do expect { create_and_send! }.to change(described_class, :count).by(1) @@ -73,22 +75,55 @@ it "enqueues an email per parent who gave consent" do expect { create_and_send! }.to have_delivered_email( :session_school_reminder - ).with(parent:, patient:, session:, sent_by: current_user) + ).with(parent:, patient:, programmes:, session:, sent_by: current_user) end it "enqueues a text per parent" do expect { create_and_send! }.to have_delivered_sms( :session_school_reminder - ).with(parent:, patient:, session:, sent_by: current_user) + ).with(parent:, patient:, programmes:, session:, sent_by: current_user) end context "when parent doesn't want to receive updates by text" do - before { parents.each { _1.update!(phone_receive_updates: false) } } + before { parents.each { it.update!(phone_receive_updates: false) } } it "doesn't enqueues a text" do expect { create_and_send! }.not_to have_delivered_sms end end + + context "with multiple programmes but only one eligible for vaccination" do + let(:consented_programmes) { [programme] } + + # No consent for MenACWY + let(:programmes) do + consented_programmes + [create(:programme, :menacwy)] + end + + it "enqueues an email per parent who gave consent" do + expect { create_and_send! }.to have_delivered_email( + :session_school_reminder + ).with( + parent:, + patient:, + programmes: consented_programmes, + session:, + sent_by: current_user + ) + end + + it "enqueues a text per parent" do + expect { create_and_send! }.to have_delivered_sms( + :session_school_reminder + ).with( + parent:, + patient:, + programmes: consented_programmes, + session:, + sent_by: current_user + ) + end + end end context "with an initial clinic invitation" do @@ -110,11 +145,13 @@ ).with( parent: parents.first, patient:, + programmes:, session:, sent_by: current_user ).and have_delivered_email(:session_clinic_initial_invitation).with( parent: parents.second, patient:, + programmes:, session:, sent_by: current_user ) @@ -126,11 +163,13 @@ ).with( parent: parents.first, patient:, + programmes:, session:, sent_by: current_user ).and have_delivered_sms(:session_clinic_initial_invitation).with( parent: parents.second, patient:, + programmes:, session:, sent_by: current_user ) @@ -144,7 +183,13 @@ it "still enqueues a text" do expect { create_and_send! }.to have_delivered_sms( :session_clinic_initial_invitation - ).with(parent:, patient:, session:, sent_by: current_user) + ).with( + parent:, + patient:, + programmes:, + session:, + sent_by: current_user + ) end end end @@ -168,11 +213,13 @@ ).with( parent: parents.first, patient:, + programmes:, session:, sent_by: current_user ).and have_delivered_email(:session_clinic_subsequent_invitation).with( parent: parents.second, patient:, + programmes:, session:, sent_by: current_user ) @@ -184,11 +231,13 @@ ).with( parent: parents.first, patient:, + programmes:, session:, sent_by: current_user ).and have_delivered_sms(:session_clinic_subsequent_invitation).with( parent: parents.second, patient:, + programmes:, session:, sent_by: current_user ) @@ -202,7 +251,13 @@ it "still enqueues a text" do expect { create_and_send! }.to have_delivered_sms( :session_clinic_subsequent_invitation - ).with(parent:, patient:, session:, sent_by: current_user) + ).with( + parent:, + patient:, + programmes:, + session:, + sent_by: current_user + ) end end end From 7b99d8e3d33a00026717212df35332c484a6f9c2 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 10 Jul 2025 10:07:15 +0100 Subject: [PATCH 46/50] Add Patient#approved_vaccine_methods This adds a method which encapsulates the logic related to determining the vaccine methods that are approved for a particular patient. This refactors existing the logic, but this method will then be used when generating personalisation variables for the emails and texts. --- .../app_vaccinate_form_component.rb | 15 ++---- app/models/patient.rb | 10 ++++ spec/models/patient_spec.rb | 51 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/components/app_vaccinate_form_component.rb b/app/components/app_vaccinate_form_component.rb index 22ef3bd885..8f30fdc77e 100644 --- a/app/components/app_vaccinate_form_component.rb +++ b/app/components/app_vaccinate_form_component.rb @@ -19,16 +19,11 @@ def url end def delivery_method - triage_status = patient.triage_status(programme:) - - status = - if triage_status.not_required? - patient.consent_status(programme:) - else - triage_status - end - - status.vaccine_method_nasal? ? :nasal_spray : :intramuscular + if patient.approved_vaccine_methods(programme:).include?("nasal") + :nasal_spray + else + :intramuscular + end end def dose_sequence diff --git a/app/models/patient.rb b/app/models/patient.rb index ef448929ec..a4d2b52be6 100644 --- a/app/models/patient.rb +++ b/app/models/patient.rb @@ -338,6 +338,16 @@ def consent_given_and_safe_to_vaccinate?(programme:) ) end + def approved_vaccine_methods(programme:) + triage_status = triage_status(programme:) + + if triage_status.not_required? + consent_status(programme:).vaccine_methods + else + [triage_status.vaccine_method].compact + end + end + def deceased? date_of_death != nil end diff --git a/spec/models/patient_spec.rb b/spec/models/patient_spec.rb index 83114a3905..3278e0171f 100644 --- a/spec/models/patient_spec.rb +++ b/spec/models/patient_spec.rb @@ -321,6 +321,57 @@ it { should eq("JD") } end + describe "#approved_vaccine_methods" do + subject(:approved_vaccine_methods) do + patient.approved_vaccine_methods(programme:) + end + + let(:patient) { create(:patient) } + let(:programme) { create(:programme) } + + it { should be_empty } + + context "when consent given and triage not required" do + before do + create( + :patient_consent_status, + :given, + patient:, + programme:, + vaccine_methods: %w[nasal injection] + ) + end + + it { should eq(%w[nasal injection]) } + end + + context "when consent given and triage required" do + before do + create( + :patient_consent_status, + :given, + patient:, + programme:, + vaccine_methods: %w[nasal injection] + ) + create(:patient_triage_status, :required, patient:, programme:) + end + + it { should be_empty } + + context "and when triaged" do + before do + patient.triage_status(programme:).update!( + status: "safe_to_vaccinate", + vaccine_method: "nasal" + ) + end + + it { should eq(%w[nasal]) } + end + end + end + describe "#update_from_pds!" do subject(:update_from_pds!) { patient.update_from_pds!(pds_patient) } From 0c83de41a437adaac8491b288eb8e361d6b04744 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 10 Jul 2025 10:07:27 +0100 Subject: [PATCH 47/50] Use Patient#approved_vaccine_methods This ensures that when sending emails that list vaccine side effects, we're only include the side effects for the vaccines that the patient is approved for. In most cases this has no effect, but for Flu it's possible to be approved for either nasal or injection vaccines and therefore we should only show the approriate vaccine methods. Jira-Issue: MAV-1355 --- app/lib/govuk_notify_personalisation.rb | 13 +++++++++- spec/lib/govuk_notify_personalisation_spec.rb | 26 ++++++++++++++----- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index a6c6106cc7..c68a32ce5d 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -293,7 +293,18 @@ def vaccine_side_effects if vaccination_record vaccination_record.vaccine&.side_effects elsif programmes.present? - Vaccine.where(programme: programmes).flat_map(&:side_effects) + if patient + programmes.flat_map do |programme| + # We pick the first method as it's the one most likely to be used + # to vaccinate the patient. For example, in the case of Flu, the + # parents will approve nasal (and then optionally injection). + method = patient.approved_vaccine_methods(programme:).first + + Vaccine.where(programme:, method:).flat_map(&:side_effects) + end + else + Vaccine.where(programme: programmes).flat_map(&:side_effects) + end end return if side_effects.nil? diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index 8a50beb325..200017e7cc 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -248,13 +248,27 @@ programmes.first.vaccines.first.update!(side_effects: %w[swelling unwell]) end - it do - expect(to_h).to match( - hash_including( - vaccine_side_effects: - "- generally feeling unwell\n- swelling or pain where the injection was given" + it { should include(vaccine_side_effects: "") } + + context "with injection as an approved vaccine method" do + before do + create( + :patient_triage_status, + :safe_to_vaccinate, + :injection, + patient:, + programme: programmes.first ) - ) + end + + it do + expect(to_h).to match( + hash_including( + vaccine_side_effects: + "- generally feeling unwell\n- swelling or pain where the injection was given" + ) + ) + end end end end From 78db4769ddd3009f379ebd6f1e702f1b174ae4f8 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 10 Jul 2025 12:07:48 +0100 Subject: [PATCH 48/50] Add vaccine_is_injection and vaccine_is_nasal This adds two new personalisation variables that will be sent to GOV.UK Notify when sending an email or text message to allow for the templates to be customised according to the type of vaccine that will be given to the patient. Jira-Issue: MAV-1355 --- app/lib/govuk_notify_personalisation.rb | 29 ++++++++- spec/lib/govuk_notify_personalisation_spec.rb | 60 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index c68a32ce5d..d90d79f974 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -2,8 +2,8 @@ class GovukNotifyPersonalisation include Rails.application.routes.url_helpers - include PhoneHelper + include PhoneHelper include VaccinationRecordsHelper def initialize( @@ -66,6 +66,8 @@ def to_h team_phone:, today_or_date_of_vaccination:, vaccination:, + vaccine_is_injection:, + vaccine_is_nasal:, vaccine_side_effects: }.compact end @@ -288,6 +290,30 @@ def vaccination ].join(" ") end + def vaccine_is_injection = vaccine_is?("injection") + + def vaccine_is_nasal = vaccine_is?("nasal") + + def vaccine_is?(method) + if vaccination_record + vaccination_record.vaccine&.method == method ? "yes" : "no" + elsif programmes.present? + any_vaccines_with_method = + if patient + programmes.any? do |programme| + # We pick the first method as it's the one most likely to be used + # to vaccinate the patient. For example, in the case of Flu, the + # parents will approve nasal (and then optionally injection). + patient.approved_vaccine_methods(programme:).first == method + end + else + Vaccine.where(programme: programmes, method:).exists? + end + + any_vaccines_with_method ? "yes" : "no" + end + end + def vaccine_side_effects side_effects = if vaccination_record @@ -299,7 +325,6 @@ def vaccine_side_effects # to vaccinate the patient. For example, in the case of Flu, the # parents will approve nasal (and then optionally injection). method = patient.approved_vaccine_methods(programme:).first - Vaccine.where(programme:, method:).flat_map(&:side_effects) end else diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index 200017e7cc..2cc8919168 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -74,6 +74,8 @@ team_name: "Organisation", team_phone: "01234 567890 (option 1)", vaccination: "HPV vaccination", + vaccine_is_injection: "no", + vaccine_is_nasal: "no", vaccine_side_effects: "" } ) @@ -243,6 +245,64 @@ end end + context "with vaccine methods" do + context "and an injection-only programme" do + before do + create( + :patient_consent_status, + :given, + patient:, + programme: programmes.first + ) + end + + it { should include(vaccine_is_injection: "yes", vaccine_is_nasal: "no") } + end + + context "and a nasal spray programme" do + let(:programmes) { [create(:programme, :flu)] } + + before do + create( + :patient_consent_status, + :given, + patient:, + programme: programmes.first, + vaccine_methods: %w[nasal injection] + ) + end + + it { should include(vaccine_is_injection: "no", vaccine_is_nasal: "yes") } + end + + context "and multiple programmes" do + let(:programmes) { [create(:programme, :hpv), create(:programme, :flu)] } + + before do + create( + :patient_consent_status, + :given, + patient:, + programme: programmes.first, + vaccine_methods: %w[nasal injection] + ) + create( + :patient_consent_status, + :given, + patient:, + programme: programmes.second + ) + end + + it do + expect(to_h).to include( + vaccine_is_injection: "yes", + vaccine_is_nasal: "yes" + ) + end + end + end + context "with vaccine side effects" do before do programmes.first.vaccines.first.update!(side_effects: %w[swelling unwell]) From d5ba56382a4d8320b979fb990449528241e80ebb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 21:15:56 +0000 Subject: [PATCH 49/50] Bump @playwright/test from 1.53.2 to 1.54.0 Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.53.2 to 1.54.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.53.2...v1.54.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.54.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 645ec61dac..4636ed242a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@axe-core/playwright": "^4.10.2", - "@playwright/test": "^1.53.2", + "@playwright/test": "^1.54.0", "@prettier/plugin-ruby": "^4.0.4", "@types/jest": "^30.0.0", "esbuild-jest": "^0.5.0", diff --git a/yarn.lock b/yarn.lock index 4c1fc81f47..95ec119830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2096,12 +2096,12 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058" integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== -"@playwright/test@^1.53.2": - version "1.53.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.53.2.tgz#fafb8dd5e109fc238c4580f82bebc2618f929f77" - integrity sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw== +"@playwright/test@^1.54.0": + version "1.54.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.54.0.tgz#e5d824402c8586172b53a449a1c892237a7fe18c" + integrity sha512-6Mnd5daQmLivaLu5kxUg6FxPtXY4sXsS5SUwKjWNy4ISe4pKraNHoFxcsaTFiNUULbjy0Vlb5HT86QuM0Jy1pQ== dependencies: - playwright "1.53.2" + playwright "1.54.0" "@prettier/plugin-ruby@^4.0.4": version "4.0.4" @@ -5442,17 +5442,22 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.53.2, playwright-core@^1.53.2: +playwright-core@1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.54.0.tgz#a019b51d537250d809bbd5f612f5bc712bcbff7b" + integrity sha512-uiWpWaJh3R3etpJ0QrpligEMl62Dk1iSAB6NUXylvmQz+e3eipXHDHvOvydDAssb5Oqo0E818qdn0L9GcJSTyA== + +playwright-core@^1.53.2: version "1.53.2" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.2.tgz#78f71e2f727713daa8d360dc11c460022c13cf91" integrity sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw== -playwright@1.53.2: - version "1.53.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.53.2.tgz#cc2ef4a22da1ae562e0ed91edb9e22a7c4371305" - integrity sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A== +playwright@1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.54.0.tgz#cd1538103c872d02ab22bf3bcb8abfc5705b336b" + integrity sha512-y9yzHmXRwEUOpghM7XGcA38GjWuTOUMaTIcm/5rHcYVjh5MSp9qQMRRMc/+p1cx+csoPnX4wkxAF61v5VKirxg== dependencies: - playwright-core "1.53.2" + playwright-core "1.54.0" optionalDependencies: fsevents "2.3.2" From ff382f875090c3ef6c3b43c42f1a48cc87ed3a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 07:53:30 +0000 Subject: [PATCH 50/50] Bump playwright-core from 1.53.2 to 1.54.0 Bumps [playwright-core](https://github.com/microsoft/playwright) from 1.53.2 to 1.54.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.53.2...v1.54.0) --- updated-dependencies: - dependency-name: playwright-core dependency-version: 1.54.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4636ed242a..23a2ecdc93 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "jest-fetch-mock": "^3.0.3", "mutationobserver-shim": "^0.3.7", "officecrypto-tool": "^0.0.18", - "playwright-core": "^1.53.2", + "playwright-core": "^1.54.0", "prettier": "^3.6.2" }, "jest": { diff --git a/yarn.lock b/yarn.lock index 95ec119830..25d9454134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5442,16 +5442,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.54.0: +playwright-core@1.54.0, playwright-core@^1.54.0: version "1.54.0" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.54.0.tgz#a019b51d537250d809bbd5f612f5bc712bcbff7b" integrity sha512-uiWpWaJh3R3etpJ0QrpligEMl62Dk1iSAB6NUXylvmQz+e3eipXHDHvOvydDAssb5Oqo0E818qdn0L9GcJSTyA== -playwright-core@^1.53.2: - version "1.53.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.2.tgz#78f71e2f727713daa8d360dc11c460022c13cf91" - integrity sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw== - playwright@1.54.0: version "1.54.0" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.54.0.tgz#cd1538103c872d02ab22bf3bcb8abfc5705b336b"