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/.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" 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/Gemfile.lock b/Gemfile.lock index 13c04c2017..9c321369e4 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.533.0) + aws-sdk-ec2 (1.537.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) aws-sdk-ecr (1.104.0) @@ -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) @@ -229,15 +229,15 @@ 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 (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) @@ -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) + flipper-active_record (1.3.5) activerecord (>= 4.2, < 9) - flipper (~> 1.3.4) - flipper-ui (1.3.4) + flipper (~> 1.3.5) + 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) @@ -423,13 +423,13 @@ 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) racc pg (1.5.9) - phonelib (0.10.9) + phonelib (0.10.10) pp (0.6.2) prettyprint prettier_print (1.2.1) @@ -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) @@ -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) @@ -637,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) 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/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/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb index 56c896bafb..f99150c655 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 + EnqueueSyncVaccinationRecordToNHS.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/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/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..878e77a8da --- /dev/null +++ b/app/lib/enqueue_sync_vaccination_record_to_nhs.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module EnqueueSyncVaccinationRecordToNHS + PROGRAMME_TYPES = %w[flu hpv].freeze + + def self.call(vaccination_record) + 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 +end diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index 194a78a3be..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( @@ -65,7 +65,10 @@ def to_h team_name:, team_phone:, today_or_date_of_vaccination:, - vaccination: + vaccination:, + vaccine_is_injection:, + vaccine_is_nasal:, + vaccine_side_effects: }.compact end @@ -286,4 +289,54 @@ def vaccination programmes.count == 1 ? "vaccination" : "vaccinations" ].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 + vaccination_record.vaccine&.side_effects + elsif programmes.present? + 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? + + descriptions = + side_effects.map { Vaccine.human_enum_name(:side_effect, it) }.sort.uniq + + descriptions.map { "- #{it}" }.join("\n") + end end 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/app/lib/nhs/immunisations_api.rb b/app/lib/nhs/immunisations_api.rb index 06b0e4cc70..76b0f95751 100644 --- a/app/lib/nhs/immunisations_api.rb +++ b/app/lib/nhs/immunisations_api.rb @@ -1,43 +1,67 @@ # frozen_string_literal: true module NHS::ImmunisationsAPI - class PatientNotFound < StandardError - end - 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" - ) - 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 + 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", + 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 record #{vaccination_record.id} to" \ + " Immunisations API: #{diagnostics}" + else + raise + end end - def extract_error_info(response_body) - return { code: nil, diagnostics: "No response body" } unless response_body + private - response = JSON.parse(response_body, symbolize_names: true) + def extract_error_diagnostics(response) + return nil if response.nil? || response[:body].blank? - 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 + begin + JSON.parse(response[:body], symbolize_names: true).dig( + :issue, + 0, + :diagnostics + ) + rescue JSON::ParserError + nil + end + end - { code: response[:issue].first[:code], diagnostics: diagnostics } + def extract_nhs_id(location) + if (match = location.match(%r{Immunization/([a-f0-9-]+)})) + match[1] + else + raise UnrecognisedLocation, location end end end 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/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/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.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/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/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/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 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/app/models/vaccination_record.rb b/app/models/vaccination_record.rb index 40e1e6c18a..34d44f272f 100644 --- a/app/models/vaccination_record.rb +++ b/app/models/vaccination_record.rb @@ -4,41 +4,45 @@ # # 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_etag :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 +# nhs_immunisations_api_id :string +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # 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/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/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/config/feature_flags.yml b/config/feature_flags.yml index 4d4c33f6a5..91ff963aa1 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -8,3 +8,9 @@ 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. + +sync_vaccination_records_to_nhs_on_create: Send new vaccinations recorded by + nurses to NHS Immunisations FHIR API. 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", 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/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/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/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/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 2e385f7c1f..cfe10734c0 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,8 +820,12 @@ t.string "performed_ods_code" t.bigint "vaccine_id" 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 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" @@ -842,6 +846,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/docs/disaster-recovery.md b/docs/disaster-recovery.md index aa6ccee3c4..2f7ef3250e 100644 --- a/docs/disaster-recovery.md +++ b/docs/disaster-recovery.md @@ -41,6 +41,35 @@ 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. +- Copy the arn of the DB snapshot and add it to the `snapshot_identifier` in the `aws_rds_cluster core` resource block in + [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 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. + +## 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). + - 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. + ## Getting a local dump of an Aurora DB You need Postgres 16+ to connect to the Aurora DB. @@ -195,16 +224,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. 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? diff --git a/package.json b/package.json index e3cb2d9075..23a2ecdc93 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", @@ -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", @@ -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/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 diff --git a/spec/factories/vaccination_records.rb b/spec/factories/vaccination_records.rb index ef5e6e37a8..3681852ea9 100644 --- a/spec/factories/vaccination_records.rb +++ b/spec/factories/vaccination_records.rb @@ -4,41 +4,45 @@ # # 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_etag :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 +# nhs_immunisations_api_id :string +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # 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/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/features/cli_vaccination_records_sync_spec.rb b/spec/features/cli_vaccination_records_sync_spec.rb new file mode 100644 index 0000000000..9cdbba047d --- /dev/null +++ b/spec/features/cli_vaccination_records_sync_spec.rb @@ -0,0 +1,130 @@ +# 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: 201, + body: "", + headers: { + location: + "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 + @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 diff --git a/spec/features/flu_vaccination_administered_spec.rb b/spec/features/flu_vaccination_administered_spec.rb index 57cfc8532c..cb15ecc2dc 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_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_nhs 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_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 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_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 bbad97e0f3..9604cad838 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_nhs_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_nhs 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_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 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_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 8ec0704c8f..33261acde9 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_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 @@ -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_nhs 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_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 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_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 900d67a4c8..02dfc52a25 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_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 @@ -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_nhs 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_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 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_nhs + assert_no_enqueued_jobs(only: SyncVaccinationRecordToNHSJob) + end end 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 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 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..9614aae391 --- /dev/null +++ b/spec/lib/enqueue_sync_vaccination_record_to_nhs_spec.rb @@ -0,0 +1,126 @@ +# 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(: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 "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) + 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 + + 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/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index f674763f1b..2cc8919168 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -73,7 +73,10 @@ team_email: "organisation@example.com", team_name: "Organisation", team_phone: "01234 567890 (option 1)", - vaccination: "HPV vaccination" + vaccination: "HPV vaccination", + vaccine_is_injection: "no", + vaccine_is_nasal: "no", + vaccine_side_effects: "" } ) end @@ -241,4 +244,91 @@ ) 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]) + end + + 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 diff --git a/spec/lib/nhs/immunisations_api_spec.rb b/spec/lib/nhs/immunisations_api_spec.rb index 7a5d0d3e7c..7ccfe6a579 100644 --- a/spec/lib/nhs/immunisations_api_spec.rb +++ b/spec/lib/nhs/immunisations_api_spec.rb @@ -47,8 +47,23 @@ 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 { Flipper.enable(:immunisations_fhir_api_integration) } + it "sends the correct JSON payload" do expected_body = File.read(Rails.root.join("spec/fixtures/fhir/immunisation.json")).chomp @@ -56,132 +71,144 @@ # 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 - 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 + it "stores the id from the response" do + described_class.record_immunisation(vaccination_record) - context "generic error" do - let(:code) { "invalid" } - let(:diagnostics) { "Invalid patient ID" } + expect( + vaccination_record.nhs_immunisations_api_id + ).to eq "ffff1111-eeee-2222-dddd-3333eeee4444" + end - it "raises an error with the correct message" do - begin - described_class.record_immunisation(vaccination_record) - rescue StandardError - nil - end + it "stores the nhs_immunisations_api_synced_at from the response" do + freeze_time do + described_class.record_immunisation(vaccination_record) - expect(Rails.logger).to have_received(:error).with( - /\[invalid\] Invalid patient ID/ - ) - end + expect( + vaccination_record.nhs_immunisations_api_synced_at + ).to eq Time.current 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" - end + it "initialises the etag to 1" do + described_class.record_immunisation(vaccination_record) - it "raises an error with the correct message" do - begin - described_class.record_immunisation(vaccination_record) - rescue StandardError - nil - end + expect(vaccination_record.nhs_immunisations_api_etag).to eq "1" + end - expect(Rails.logger).to have_received(:error).with( - /\[exception\] NHS Number is invalid or it doesn't exist/ - ) - end + context "an error is returned by the api" do + 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 - end - end - describe "extract_error_info" do - subject(:error_info) { described_class.extract_error_info(response) } + let(:status) { 201 } + let(:code) { nil } + let(:diagnostics) { nil } - context "response body has an 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", - diagnostics: "Invalid patient ID" + details: { + coding: [ + { + system: "https://fhir.nhs.uk/Codesystem/http-error-codes", + code: + } + ] + }, + diagnostics: } ] }.to_json end - its([:code]) { should eq "invalid" } - its([:diagnostics]) { should eq "Invalid patient ID" } - end + context "unexpected response status" do + let(:status) { 200 } + let(:response) { "" } - context "when the response body is empty" do - let(:response) { nil } + 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 - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No response body" } - end + context "4XX error" do + let(:status) { 404 } + let(:diagnostics) { "Invalid patient ID" } - context "when the response body has no issue attribute" do - let(:response) { "{}" } + it "raises an error with the diagnostic message" do + expect { + described_class.record_immunisation(vaccination_record) + }.to raise_error( + StandardError, + "Error syncing vaccination record #{vaccination_record.id} to" \ + " Immunisations API: Invalid patient ID" + ) + end + end - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No response body" } + 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 diagnostic message" do + expect { + described_class.record_immunisation(vaccination_record) + }.to raise_error(Faraday::Error) + end + end end - context "when the response body has no issues" do - let(:response) { '{"issues": [] }' } + context "the immunisations_fhir_api_integration feature flag is disabled" do + before { Flipper.disable(:immunisations_fhir_api_integration) } - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "No issues in response" } - end + it "does not make a request to the NHS API" do + described_class.record_immunisation(vaccination_record) - context "the issue severity is not 'error'" do - let(:response) do - { - issue: [ - { - severity: "warning", - code: "not-found", - diagnostics: "Patient not found" - } - ] - }.to_json + expect(stubbed_request).not_to have_been_made end - - its([:code]) { should be_nil } - its([:diagnostics]) { should eq "Issue is not an error" } end end end 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/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 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) } 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 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 diff --git a/spec/models/vaccination_record_spec.rb b/spec/models/vaccination_record_spec.rb index 840fe24078..b63cfe8f66 100644 --- a/spec/models/vaccination_record_spec.rb +++ b/spec/models/vaccination_record_spec.rb @@ -4,41 +4,45 @@ # # 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_etag :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 +# nhs_immunisations_api_id :string +# patient_id :bigint +# performed_by_user_id :bigint +# programme_id :bigint not null +# session_id :bigint +# vaccine_id :bigint # # 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/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 diff --git a/terraform/account/main.tf b/terraform/account/main.tf index dabd2606ae..dcaeb37ac6 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/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", 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/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/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 } 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/app/rds.tf b/terraform/app/rds.tf index e5f7b33ce5..7051016bcd 100644 --- a/terraform/app/rds.tf +++ b/terraform/app/rds.tf @@ -48,7 +48,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" diff --git a/terraform/app/variables.tf b/terraform/app/variables.tf index a578f1b653..0c309810c6 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 { rds_cluster = "mavis-${var.environment}" db_instances = { 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..24e8a09f2a 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" } } @@ -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..dab4043b9e 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" } } @@ -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 }, 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/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] + } } 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" } } } diff --git a/yarn.lock b/yarn.lock index 726b620d10..25d9454134 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" @@ -2091,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" @@ -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" @@ -5436,17 +5442,17 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.53.2, 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-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@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"