diff --git a/.github/workflows/create_dockerized_db.yml b/.github/workflows/build-and-push-database-image.yml similarity index 70% rename from .github/workflows/create_dockerized_db.yml rename to .github/workflows/build-and-push-database-image.yml index cb12738052..d0d3fd32cf 100644 --- a/.github/workflows/create_dockerized_db.yml +++ b/.github/workflows/build-and-push-database-image.yml @@ -1,11 +1,10 @@ -name: Create Dockerized Database -run-name: Creating dockerized image from ${{ github.ref_name }} +name: Build and push database image +run-name: Build and push database image for ${{ github.ref_name }} on: workflow_dispatch: push: - branches: - - next + branches: [next] workflow_call: inputs: github_ref: @@ -17,8 +16,8 @@ permissions: contents: read jobs: - setup-development-database: - name: Setup Development Database + build-and-push-database-image: + name: Build and push database image runs-on: ubuntu-latest env: RAILS_ENV: development @@ -32,34 +31,36 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.github_ref || github.ref_name == 'next' && 'next' || github.ref_name }} - repository: nhsuk/manage-vaccinations-in-schools + repository: nhsdigital/manage-vaccinations-in-schools - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: .tool-versions cache: yarn - - name: Build custom postgres image - run: | - echo -e "FROM postgres:16.11\n\nENV PGDATA=\"/var/lib/postgresql/mydata\"" > db.Dockerfile - docker build -t custom-postgres:latest -f db.Dockerfile . - - name: Start db container + - name: Create Dockerfile + run: >- + echo -e \ + "FROM postgis/postgis:17-master\nENV PGDATA=\"/var/lib/postgresql/mydata\"" \ + > database.Dockerfile + - name: Build image + run: docker build -t database:latest -f database.Dockerfile . + - name: Start container run: | docker run -d \ --name database \ -e "POSTGRES_HOST_AUTH_METHOD=trust" \ -p 5432:5432 \ - custom-postgres:latest - - name: Wait for db to be ready + database:latest + - name: Wait for database to be ready run: | docker exec database bash -c ' until pg_isready -U postgres; do echo "Waiting for postgres..." sleep 2 - done - ' + done' - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0 with: bundler-cache: true - - name: Populate database for testing + - name: Set up database for testing run: | bin/rails db:setup bin/rails feature_flags:enable_for_development @@ -73,17 +74,17 @@ jobs: id: login-ecr uses: aws-actions/amazon-ecr-login@376925c9d111252e87ae59691e5a442dd100ef6a # v2.1.3 # yamllint disable rule:line-length - - name: get github ref short - id: github-ref + - name: Get image tag + id: get-image-tag run: | git_ref=$(git rev-parse ${{ inputs.github_ref || github.ref_name == 'next' && 'origin/next' || github.ref_name }}) - echo "ref=$git_ref" >> "$GITHUB_OUTPUT" - - name: Commit postgres container with database + echo "value=$git_ref" >> "$GITHUB_OUTPUT" + - name: Commit image run: >- docker commit database "${{ steps.login-ecr.outputs.registry - }}/mavis/development/postgres_db:${{ steps.github-ref.outputs.ref }}" + }}/mavis/development/postgres_db:${{ steps.get-image-tag.outputs.value }}" - name: Push image run: >- docker push "${{ steps.login-ecr.outputs.registry }}/mavis/development/postgres_db:${{ - steps.github-ref.outputs.ref }}" + steps.get-image-tag.outputs.value }}" # yamllint enable rule:line-length diff --git a/.github/workflows/end-to-end-tests-aws.yml b/.github/workflows/end-to-end-tests-aws.yml index 28c9740c4d..4cfda7d7f4 100644 --- a/.github/workflows/end-to-end-tests-aws.yml +++ b/.github/workflows/end-to-end-tests-aws.yml @@ -1,4 +1,4 @@ -name: AWS deployment E2E tests +name: AWS on: workflow_call: @@ -122,7 +122,7 @@ jobs: permissions: id-token: write contents: read - uses: ./.github/workflows/create_dockerized_db.yml + uses: ./.github/workflows/build-and-push-database-image.yml with: github_ref: ${{ needs.check-database-image-presence.outputs.db_git_ref_sha }} launch-dockerized-devimage: diff --git a/.github/workflows/end-to-end-tests-local.yml b/.github/workflows/end-to-end-tests-local.yml index b6793a5f6c..5593ba4e33 100644 --- a/.github/workflows/end-to-end-tests-local.yml +++ b/.github/workflows/end-to-end-tests-local.yml @@ -1,4 +1,4 @@ -name: Local E2E tests +name: Local on: [workflow_call] @@ -6,8 +6,8 @@ jobs: end-to-end-tests: runs-on: ubuntu-latest services: - postgres: - image: postgres:17.2 + postgresql: + image: postgis/postgis:17-master env: POSTGRES_PASSWORD: postgres options: >- diff --git a/.github/workflows/end-to-end-tests-on-pull-request.yml b/.github/workflows/end-to-end-tests-on-pull-request.yml index d5ef817deb..5af4fb511a 100644 --- a/.github/workflows/end-to-end-tests-on-pull-request.yml +++ b/.github/workflows/end-to-end-tests-on-pull-request.yml @@ -1,4 +1,4 @@ -name: E2E tests on PR +name: End-to-end tests on: [pull_request] @@ -11,7 +11,7 @@ permissions: {} jobs: aws-e2e-flow: if: github.event.pull_request.head.repo.full_name == github.repository - name: AWS deployment E2E tests + name: AWS permissions: id-token: write contents: write @@ -22,7 +22,7 @@ jobs: local-e2e-flow: if: github.event.pull_request.head.repo.full_name != github.repository - name: Local E2E tests + name: Local uses: ./.github/workflows/end-to-end-tests-local.yml ensure-run-success: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e8a2a8a19..29709bd1d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,8 @@ jobs: name: RSpec runs-on: ubuntu-latest services: - postgres: - image: postgres:17.2 + postgresql: + image: postgis/postgis:17-master env: POSTGRES_PASSWORD: postgres options: >- @@ -50,8 +50,8 @@ jobs: name: Seeds runs-on: ubuntu-latest services: - postgres: - image: postgres:17.2 + postgresql: + image: postgis/postgis:17-master env: POSTGRES_PASSWORD: postgres options: >- @@ -60,7 +60,8 @@ jobs: - 5432:5432 env: RAILS_ENV: development - DATABASE_URL: postgres://postgres:postgres@localhost:5432/manage_vaccinations_development + DATABASE_URL: >- + postgis://postgres:postgres@localhost:5432/manage_vaccinations_development steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -70,7 +71,7 @@ jobs: - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0 with: bundler-cache: true - - name: Check seeds run + - name: Run seeds run: bin/rails db:prepare jest: @@ -82,5 +83,7 @@ jobs: with: node-version-file: .tool-versions cache: yarn - - run: yarn install --immutable --immutable-cache --check-cache - - run: yarn test + - name: Install dependencies + run: yarn install --immutable --immutable-cache --check-cache + - name: Run tests + run: yarn test diff --git a/.tool-versions b/.tool-versions index 81e3b2884f..b5b4d32aba 100644 --- a/.tool-versions +++ b/.tool-versions @@ -3,9 +3,9 @@ awscli 2.27.46 hk 1.37.0 nodejs 22.15.0 pkl 0.31.0 -postgres 17.2 python 3.14.4 -redis 8.2.1 ruby 4.0.3 shellcheck 0.11.0 yamllint 1.38.0 +yarn 1.22.22 +uv 0.11.7 diff --git a/Gemfile b/Gemfile index 85d6ad7559..f7e207a90e 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ gem "stackprof" # 3rd party gems gem "activerecord-import" +gem "activerecord-postgis-adapter" gem "activerecord-session_store" gem "active_record_union" gem "amazing_print" diff --git a/Gemfile.lock b/Gemfile.lock index 5db0fb4bdb..c55d1db4e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,6 +90,9 @@ GEM timeout (>= 0.4.0) activerecord-import (2.2.0) activerecord (>= 4.2) + activerecord-postgis-adapter (11.1.1) + activerecord (~> 8.1.0) + rgeo-activerecord (~> 8.1.0) activerecord-session_store (2.2.0) actionpack (>= 7.0) activerecord (>= 7.0) @@ -175,14 +178,14 @@ GEM protocol-websocket (~> 0.17) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1238.0) + aws-partitions (1.1240.0) aws-sdk-accessanalyzer (1.88.0) aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-cloudwatch (1.133.0) + aws-sdk-cloudwatch (1.134.0) aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-core (3.244.0) + aws-sdk-core (3.246.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -202,7 +205,7 @@ GEM aws-sdk-kms (1.123.0) aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-rds (1.310.0) + aws-sdk-rds (1.311.0) aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.219.0) @@ -294,7 +297,7 @@ GEM dry-cli (1.4.1) email_validator (2.2.4) activemodel - erb (6.0.3) + erb (6.0.4) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -306,7 +309,7 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.7.1) + faker (3.8.0) i18n (>= 1.8.11, < 2) falcon (0.55.3) async @@ -537,7 +540,7 @@ GEM parallel (1.28.0) parallel_tests (5.7.0) parallel - parser (3.3.10.2) + parser (3.3.11.1) ast (~> 2.4.1) racc pg (1.6.3-arm64-darwin) @@ -548,7 +551,7 @@ GEM prettier_print (1.2.1) prettyprint (0.2.0) prism (1.9.0) - propshaft (1.3.1) + propshaft (1.3.2) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -664,6 +667,10 @@ GEM reverse_markdown (3.0.2) nokogiri rexml (3.4.4) + rgeo (3.1.0) + rgeo-activerecord (8.1.0) + activerecord (>= 8.1, < 8.2) + rgeo (>= 3.0) rladr (1.2.0) rspec (3.13.2) rspec-core (~> 3.13.0) @@ -694,7 +701,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.7) - rubocop (1.82.1) + rubocop (1.86.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -702,18 +709,18 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.48.0, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-govuk (5.2.0) - rubocop (= 1.82.1) - rubocop-ast (= 1.49.0) + rubocop-govuk (5.2.1) + rubocop (= 1.86.0) + rubocop-ast (= 1.49.1) rubocop-capybara (= 2.22.1) rubocop-rails (= 2.34.3) rubocop-rake (= 0.7.1) @@ -767,13 +774,13 @@ GEM sidekiq (>= 5.0) shoulda-matchers (7.0.1) activesupport (>= 7.1) - sidekiq (8.1.2) + sidekiq (8.1.3) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) rack (>= 3.2.0) redis-client (>= 0.26.0) - sidekiq-scheduler (6.0.1) + sidekiq-scheduler (6.0.2) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) sidekiq-throttled (2.1.0) @@ -910,6 +917,7 @@ PLATFORMS DEPENDENCIES active_record_union activerecord-import + activerecord-postgis-adapter activerecord-session_store amazing_print annotaterb diff --git a/README.md b/README.md index 11ab4602b1..b3231020c4 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ This is a service used within the NHS for managing and recording school-aged vac We have two RDoc versions: -1. [next](https://nhsuk.github.io/manage-vaccinations-in-schools/rdoc/next) - useful for dev work (based off the `next` branch). -2. [release](https://nhsuk.github.io/manage-vaccinations-in-schools/rdoc/release) - useful for ops to debug live issues (based off the `release` branch). +- [next](https://nhsdigital.github.io/manage-vaccinations-in-schools/rdoc/next/) - useful for dev work (based off the `next` branch). +- [release](https://nhsdigital.github.io/manage-vaccinations-in-schools/rdoc/release/) - useful for ops to debug live issues (based off the `release` branch). ## Development @@ -29,7 +29,7 @@ This project depends on: - [Ruby on Rails](https://rubyonrails.org/) - [NodeJS](https://nodejs.org/) - [Yarn](https://yarnpkg.com/) -- [PostgreSQL](https://www.postgresql.org/) +- [PostgreSQL](https://www.postgresql.org/) with [PostGIS](https://postgis.net/) - [Redis](https://redis.io/) or [Valkey](https://valkey.io/) The instructions below assume you are using `mise` to manage the necessary @@ -48,34 +48,55 @@ bin/bundle exec rladr new title ### Installing dependencies -This project uses `mise`. Use the following to set up (replace `brew` and -package names depending on your platform): +This project uses [`mise`](https://mise.jdx.dev/) to manage tool versions. + +#### Prerequisites + +Before you can run `mise install` you might need to install some system +libraries and databases. + +##### macOS with Homebrew ```shell -# Dependencies for ruby +# Dependencies for Ruby brew install libyaml -# Dependencies for postgres -brew install gcc readline zlib curl ossp-uuid icu4c pkg-config +# PostgreSQL +brew install postgresql postgis -# Env vars for postgres -export OPENSSL_PATH=$(brew --prefix openssl) -export CMAKE_PREFIX_PATH=$(brew --prefix icu4c) -export PATH="$OPENSSL_PATH/bin:$CMAKE_PREFIX_PATH/bin:$PATH" -export LDFLAGS="-L$OPENSSL_PATH/lib $LDFLAGS" -export CPPFLAGS="-I$OPENSSL_PATH/include $CPPFLAGS" -export PKG_CONFIG_PATH="$CMAKE_PREFIX_PATH/lib/pkgconfig" -export MACOSX_DEPLOYMENT_TARGET="$(sw_vers -productVersion)" +# Redis +brew install redis -# Version manager +# Mise brew install mise +``` + +##### Debian-based Linux -# Yarn via brew as this skips installing `gpg` -brew install yarn +```shell +# Dependencies for Ruby +sudo apt install build-essential autoconf libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev libicu-dev rustc + +# PostgreSQL +sudo apt install -y postgresql-common +sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh +sudo apt install postgresql-17 postgresql-contrib postgresql-17-postgis-3 +sudo -u postgres psql -c "CREATE USER $(whoami); ALTER USER $(whoami) WITH SUPERUSER;" + +# Redis +sudo apt install redis-server + +# Mise +curl https://mise.run | sh ``` -Then to install the required tools (or update, following a change to -`.tool-versions`): +See [installing Mise](https://mise.jdx.dev/installing-mise.html) for more +details on how to install Mise. + +#### Using Mise + +At this point it should now be possible to use `mise` to install the required +versions of the tools. ```shell mise install @@ -86,28 +107,33 @@ mise install For the application to start successfully, PostgreSQL and Redis/Valkey must be running. -#### PostgreSQL +#### macOS with Homebrew -If using `brew`, the simplest option is to run `brew services start postgresql`. +```shell +brew services start postgresql redis +``` -Alternatively, you can run the server manually: +#### Debian-based Linux ```shell -pg_ctl start -psql -U postgres -c "CREATE USER $(whoami); ALTER USER $(whoami) WITH SUPERUSER;" +sudo systemctl start postgresql.service +sudo systemctl start redis.service ``` -#### Redis/Valkey +#### Manually -If using `brew`, the simplest option is to run `brew services start redis`. +```shell +pg_ctl start +psql -U postgres -c "CREATE USER $(whoami); ALTER USER $(whoami) WITH SUPERUSER;" +``` -Alternatively, you can run the server manually: +You may have to run this as root via `sudo`. ```shell redis-server ``` -### Running locally +### Running the application When running for the first time, `bin/setup` will automatically install Ruby dependencies and set up the database. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..048626f25c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security + +We take security and the protection of private data extremely seriously. If you believe you have found a vulnerability or other issue which has compromised or could compromise the security of any of our systems or private data managed by our systems, please do not hesitate to contact us using the method outlined below. + +## Reporting a vulnerability + +If you believe you have found a security issue in this repository, please report it using GitHub's private vulnerability reporting: + +1. [Report a vulnerability](https://github.com/NHSDigital/manage-vaccinations-in-schools/security/advisories/new) +1. Provide details of the issue and steps to reproduce + +This creates a private channel for discussion and allows us to coordinate a fix before any public disclosure. + +## General Security Enquiries + +If you have general enquiries regarding our cybersecurity, please reach out to us at [cybersecurity@nhs.net](cybersecurity@nhs.net) diff --git a/app/components/app_activity_log_component.rb b/app/components/app_activity_log_component.rb index 75c3487ec8..a028a77670 100644 --- a/app/components/app_activity_log_component.rb +++ b/app/components/app_activity_log_component.rb @@ -41,147 +41,12 @@ class AppActivityLogComponent < ViewComponent::Base def initialize(team:, patient:, programme_type: nil, session: nil) @patient = patient - - @archive_reasons = - @patient.archive_reasons.where(team:).includes(:created_by) - - @attendance_records = - patient - .attendance_records - .includes(:location) - .then do |scope| - session ? scope.where(location: session.location) : scope - end - - @consents = - @patient - .consents - .includes( - :consent_form, - :parent, - :recorded_by, - patient: :parent_relationships - ) - .then do |scope| - if programme_type - scope.where(programme_type:) - elsif session - scope.for_session(session) - else - scope - end - end - - @gillick_assessments = - @patient - .gillick_assessments - .includes(:performed_by) - .order(:created_at) - .then do |scope| - if programme_type - scope.where(programme_type:) - elsif session - scope.for_session(session) - else - scope - end - end - - @notes = - @patient - .notes - .includes(:created_by, :patient, :session) - .then { |scope| session ? scope.where(session:) : scope } - - @notify_log_entries = - @patient - .notify_log_entries - .includes(:sent_by) - .preload(:notify_log_entry_programmes) - .then do |scope| - if programme_type - scope.for_programme_type(programme_type) - elsif session - scope.for_session(session) - else - scope - end - end - - @patient_locations = - @patient - .patient_locations - .includes(:location) - .then do |scope| - session ? scope.where(location: session.location) : scope - end - - @patient_merge_log_entries = - @patient.patient_merge_log_entries.includes(:user) - - @patient_specific_directions = - @patient - .patient_specific_directions - .includes(:created_by) - .then do |scope| - if programme_type - scope.where(programme_type:) - elsif session - scope.for_session(session) - else - scope - end - end - - @pre_screenings = - @patient - .pre_screenings - .includes(:performed_by) - .then do |scope| - if programme_type - scope.where(programme_type:) - elsif session - scope.for_session(session) - else - scope - end - end - - @triages = - @patient - .triages - .includes(:performed_by) - .then do |scope| - if programme_type - scope.where(programme_type:) - elsif session - scope.for_session(session) - else - scope - end - end - - @vaccination_records = - @patient - .vaccination_records - .with_discarded - .includes(:performed_by_user, :vaccine) - .then { |scope| programme_type ? scope.where(programme_type:) : scope } + @team = team + @programme_type = programme_type + @session = session end - attr_reader :archive_reasons, - :consents, - :gillick_assessments, - :notes, - :notify_log_entries, - :patient, - :patient_locations, - :patient_merge_log_entries, - :patient_specific_directions, - :pre_screenings, - :attendance_records, - :triages, - :vaccination_records + attr_reader :patient def all_events [ @@ -290,6 +155,8 @@ def consent_events end def expiration_events + return [] unless include_programme_specific_events? + all_programmes = Programme.all.to_a AcademicYear.all.flat_map do |academic_year| @@ -568,6 +435,186 @@ def attendance_events private + def include_programme_specific_events? + @programme_type.present? || @session.present? + end + + def archive_reasons + return [] if include_programme_specific_events? + + @archive_reasons ||= + @patient.archive_reasons.where(team: @team).includes(:created_by) + end + + def patient_merge_log_entries + return [] if include_programme_specific_events? + + @patient_merge_log_entries ||= + @patient.patient_merge_log_entries.includes(:user) + end + + def attendance_records + return [] unless include_programme_specific_events? + + @attendance_records ||= + patient + .attendance_records + .includes(:location) + .then do |scope| + @session ? scope.where(location: @session.location) : scope + end + end + + def consents + return [] unless include_programme_specific_events? + + @consents ||= + @patient + .consents + .includes( + :consent_form, + :parent, + :recorded_by, + patient: :parent_relationships + ) + .then do |scope| + if @programme_type + scope.where(programme_type: @programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def gillick_assessments + return [] unless include_programme_specific_events? + + @gillick_assessments ||= + @patient + .gillick_assessments + .includes(:performed_by) + .order(:created_at) + .then do |scope| + if @programme_type + scope.where(programme_type: @programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def notes + return [] unless include_programme_specific_events? + + @notes ||= + @patient + .notes + .includes(:created_by, :patient, :session) + .then { |scope| @session ? scope.where(session: @session) : scope } + end + + def notify_log_entries + return [] unless include_programme_specific_events? + + @notify_log_entries ||= + @patient + .notify_log_entries + .includes(:sent_by) + .preload(:notify_log_entry_programmes) + .then do |scope| + if @programme_type + scope.for_programme_type(@programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def patient_locations + return [] unless include_programme_specific_events? + + @patient_locations ||= + @patient + .patient_locations + .includes(:location) + .then do |scope| + @session ? scope.where(location: @session.location) : scope + end + end + + def patient_specific_directions + return [] unless include_programme_specific_events? + + @patient_specific_directions ||= + @patient + .patient_specific_directions + .includes(:created_by) + .then do |scope| + if @programme_type + scope.where(programme_type: @programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def pre_screenings + return [] unless include_programme_specific_events? + + @pre_screenings ||= + @patient + .pre_screenings + .includes(:performed_by) + .then do |scope| + if @programme_type + scope.where(programme_type: @programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def triages + return [] unless include_programme_specific_events? + + @triages ||= + @patient + .triages + .includes(:performed_by) + .then do |scope| + if @programme_type + scope.where(programme_type: @programme_type) + elsif @session + scope.for_session(@session) + else + scope + end + end + end + + def vaccination_records + return [] unless include_programme_specific_events? + + @vaccination_records ||= + @patient + .vaccination_records + .with_discarded + .includes(:performed_by_user, :vaccine) + .then do |scope| + @programme_type ? scope.where(programme_type: @programme_type) : scope + end + end + def expired_items_for(academic_year:, programmes:) { consents:, diff --git a/app/components/app_patient_activity_component.rb b/app/components/app_patient_activity_component.rb new file mode 100644 index 0000000000..d0ea718c20 --- /dev/null +++ b/app/components/app_patient_activity_component.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AppPatientActivityComponent < ViewComponent::Base + def initialize(patient, team:) + @patient = patient + @team = team + end + + def call + render AppCardComponent.new(section: true) do |card| + card.with_heading { "Activity log" } + render AppActivityLogComponent.new(patient:, team:) + end + end + + private + + attr_reader :patient, :team +end diff --git a/app/controllers/api/reporting/totals_controller.rb b/app/controllers/api/reporting/totals_controller.rb index a90c840a56..012d7e13f9 100644 --- a/app/controllers/api/reporting/totals_controller.rb +++ b/app/controllers/api/reporting/totals_controller.rb @@ -148,11 +148,52 @@ def render_totals_json consent_no_response: metrics.consent_no_response, consent_refused: metrics.consent_refused, consent_conflicts: metrics.consent_conflicts, + consent_refusal_reasons: team_consent_refusal_reasons, + consent_routes: team_consent_routes, vaccinations_given: team_vaccinations_given_count, monthly_vaccinations_given: team_monthly_vaccinations_given } end + def team_consent_refusal_reasons + counts = + latest_scoped_consents + .where(response: :refused) + .where.not(reason_for_refusal: nil) + .group(:reason_for_refusal) + .count + + Consent.reason_for_refusals.keys.index_with { |key| counts[key] || 0 } + end + + def team_consent_routes + counts = latest_scoped_consents.group(:route).count + Consent.routes.keys.index_with { |key| counts[key] || 0 } + end + + def latest_scoped_consents + base = + Consent + .where(patient_id: @totals_scope.select(:patient_id)) + .where(team_id: @team&.id || current_user.team_ids) + .where(academic_year: params[:academic_year]) + .not_invalidated + .response_provided + + if params[:programme].present? + base = base.where(programme_type: params[:programme]) + end + + Consent.where( + id: + base.select( + "DISTINCT ON (patient_id, programme_type, parent_id) id" + ).order( + Arel.sql("patient_id, programme_type, parent_id, submitted_at DESC") + ) + ) + end + def apply_workgroup_filter workgroup = params[:workgroup].presence || cis2_info.team_workgroup return unless workgroup diff --git a/app/controllers/api/testing/vaccinations_search_in_nhs_controller.rb b/app/controllers/api/testing/vaccinations_search_in_nhs_controller.rb new file mode 100644 index 0000000000..647264fc8c --- /dev/null +++ b/app/controllers/api/testing/vaccinations_search_in_nhs_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class API::Testing::VaccinationsSearchInNHSController < API::Testing::BaseController + def create + if params[:wait].present? + EnqueueVaccinationsSearchInNHSJob.perform_now + render status: :ok + else + EnqueueVaccinationsSearchInNHSJob.perform_later + render status: :accepted + end + end +end diff --git a/app/controllers/draft_schools_controller.rb b/app/controllers/draft_schools_controller.rb index f46e9bcf64..3157dfa77a 100644 --- a/app/controllers/draft_schools_controller.rb +++ b/app/controllers/draft_schools_controller.rb @@ -231,7 +231,7 @@ def update_params address_town address_postcode ], - year_groups: [year_groups: []], + year_groups: [{ year_groups: [] }], confirm: [] }.fetch(current_step) diff --git a/app/jobs/email_delivery_job.rb b/app/jobs/email_delivery_job.rb index 983dc2578a..ec579135b1 100644 --- a/app/jobs/email_delivery_job.rb +++ b/app/jobs/email_delivery_job.rb @@ -103,7 +103,7 @@ def perform( subject: rendered[:subject], template_id: template.id, type: :email, - purpose: NotifyLogEntry.purpose_for_template_name(template_name_sym), + purpose: template.purpose, notify_log_entry_programmes_attributes: personalisation.programmes.map do { programme_type: it.type, disease_types: it.disease_types } diff --git a/app/jobs/enqueue_automated_careplus_reports_job.rb b/app/jobs/enqueue_automated_careplus_reports_job.rb new file mode 100644 index 0000000000..5380729b3c --- /dev/null +++ b/app/jobs/enqueue_automated_careplus_reports_job.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class EnqueueAutomatedCareplusReportsJob + include Sidekiq::Job + + sidekiq_options queue: :careplus + + def perform + Team.eligible_for_automated_careplus_reports.ids.each do |team_id| + SendAutomatedCareplusReportsJob.perform_async(team_id) + end + end +end diff --git a/app/jobs/send_automated_careplus_reports_job.rb b/app/jobs/send_automated_careplus_reports_job.rb new file mode 100644 index 0000000000..22e0901273 --- /dev/null +++ b/app/jobs/send_automated_careplus_reports_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SendAutomatedCareplusReportsJob + include Sidekiq::Job + + sidekiq_options queue: :careplus, lock: :until_executed + + def perform(team_id) + Careplus::AutomatedReportSender.call(team_id:) + end +end diff --git a/app/jobs/sms_delivery_job.rb b/app/jobs/sms_delivery_job.rb index 1b1c0cfb55..29627e0b8a 100644 --- a/app/jobs/sms_delivery_job.rb +++ b/app/jobs/sms_delivery_job.rb @@ -93,7 +93,7 @@ def perform( sent_by:, template_id: template.id, type: :sms, - purpose: NotifyLogEntry.purpose_for_template_name(template_name_sym), + purpose: template.purpose, notify_log_entry_programmes_attributes: personalisation.programmes.map do { programme_type: it.type, disease_types: it.disease_types } diff --git a/app/lib/careplus/automated_report_sender.rb b/app/lib/careplus/automated_report_sender.rb new file mode 100644 index 0000000000..fcba31ff30 --- /dev/null +++ b/app/lib/careplus/automated_report_sender.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +class Careplus::AutomatedReportSender + FailedResponseError = Class.new(StandardError) + BATCH_SIZE = 10_000 + + def self.call(...) = new(...).call + + def initialize(team_id:) + @team = Team.find(team_id) + end + + def call + return unless team.eligible_for_automated_careplus_reports? + + export_date = Date.yesterday + academic_year = export_date.academic_year + records_scope = + Reports::AutomatedCareplusExporter.vaccination_records_scope( + team:, + academic_year:, + start_date: export_date, + end_date: export_date + ) + + records_scope + .unscope(:order) + .in_batches(of: BATCH_SIZE) do |batch_scope| + batch_records = records_scope.where(id: batch_scope.select(:id)) + next if batch_records.none? + + send_batch!( + vaccination_records: batch_records, + academic_year:, + date: export_date + ) + end + end + + private + + attr_reader :team + + def send_batch!(vaccination_records:, academic_year:, date:) + csv = + Reports::AutomatedCareplusExporter.from_records( + vaccination_records:, + team:, + academic_year: + ) + programme_types = + vaccination_records.unscope(:order).distinct.pluck(:programme_type) + careplus_report = + create_export!(academic_year:, csv:, date:, programme_types:) + + attach_records!(careplus_report:, vaccination_records:) + + record_send_attempt!(careplus_report:) + + response = + Careplus::Client.send_csv( + username: team.careplus_username, + password: team.careplus_password, + namespace: team.careplus_namespace, + payload: csv + ) + + unless response.is_a?(Net::HTTPSuccess) + careplus_report.update!(status: :failed) + + raise FailedResponseError, + "CarePlus request failed with HTTP #{response.code}: #{response.message}" + end + + mark_as_sent!(careplus_report:) + rescue StandardError + careplus_report&.update!(status: :failed) + raise + end + + def create_export!(academic_year:, csv:, date:, programme_types:) + timestamp = Time.current + + CareplusReport.create!( + team:, + academic_year:, + date_from: date, + date_to: date, + programme_types:, + scheduled_at: timestamp, + status: :sending, + csv_filename: csv_filename(date:, timestamp:), + csv_data: csv + ) + end + + def attach_records!(careplus_report:, vaccination_records:) + timestamp = Time.current + + CareplusReportVaccinationRecord.insert_all!( + vaccination_records.map do |record| + { + careplus_report_id: careplus_report.id, + vaccination_record_id: record.id, + change_type: 0, + created_at: timestamp, + updated_at: timestamp + } + end + ) + end + + def record_send_attempt!(careplus_report:) + careplus_report.update!(sent_at: Time.current) + end + + def mark_as_sent!(careplus_report:) + careplus_report.update!(status: :sent) + end + + def csv_filename(date:, timestamp:) + "#{ + [ + "automated-careplus", + team.workgroup.parameterize, + date.iso8601, + timestamp.strftime("%H%M%S%6N") + ].join("-") + }.csv" + end +end diff --git a/app/lib/careplus/client.rb b/app/lib/careplus/client.rb index 456fcdab46..de4f423588 100644 --- a/app/lib/careplus/client.rb +++ b/app/lib/careplus/client.rb @@ -16,8 +16,10 @@ def initialize(username:, password:, namespace:, payload:) end def send_csv - uri = - URI.parse("#{Settings.careplus.base_url}/#{namespace}/soap.SchImms.cls") + base_url = Settings.careplus.base_url.presence or + raise "Settings.careplus.base_url is empty or has not been configured " \ + "(if this is a deployed service, the MOCK_CAREPLUS_URL environment variable may not be set)" + uri = URI.parse("#{base_url}/#{namespace}/soap.SchImms.cls") soap_body = build_soap_envelope post_soap_request(uri, soap_body) end diff --git a/app/lib/notify_template.rb b/app/lib/notify_template.rb index 61ef2e6887..b33a65e52c 100644 --- a/app/lib/notify_template.rb +++ b/app/lib/notify_template.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true class NotifyTemplate + include ActiveModel::Model + + validates :purpose, presence: true + validates :purpose, + inclusion: { + in: ->(_) { NotifyLogEntry.purposes.keys.map(&:to_sym) } + }, + if: -> { purpose.present? } + class << self def find(name, channel:) local_templates(channel)[name] @@ -43,12 +52,13 @@ def scan_templates(channel) template = new(name:, channel:, content:) next unless template.id + template.validate! hash[name] = template end end end - attr_reader :name, :channel, :id, :body, :subject + attr_reader :name, :channel, :id, :body, :subject, :purpose def initialize(name:, channel:, content:) @name = name.to_sym @@ -56,6 +66,7 @@ def initialize(name:, channel:, content:) frontmatter, @body = parse_frontmatter(content) @id = frontmatter["template_id"] @subject = frontmatter["subject"].to_s + @purpose = frontmatter["purpose"]&.to_sym end def render(personalisation) diff --git a/app/lib/ordnance_survey/places_api.rb b/app/lib/ordnance_survey/places_api.rb new file mode 100644 index 0000000000..ea79f2b8e0 --- /dev/null +++ b/app/lib/ordnance_survey/places_api.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module OrdnanceSurvey + class PlacesAPI + def self.find(...) = new.find(...) + + def initialize + @api_key = Settings.ordnance_survey.api_key + @base_url = "https://api.os.uk" + end + + def find(query) + params = { query:, format: "json" } + response = connection.get("/search/places/v1/find", params) + response.body.deep_transform_keys(&:downcase).deep_symbolize_keys + end + + private + + def connection + @connection ||= + Faraday.new(url: @base_url) do |f| + f.request :url_encoded + f.headers["Key"] = @api_key + f.response :logger if Rails.env.development? + f.response :json + f.response :raise_error + end + end + end +end diff --git a/app/lib/patient_status_updater.rb b/app/lib/patient_status_updater.rb index b9102b0871..6d7d948299 100644 --- a/app/lib/patient_status_updater.rb +++ b/app/lib/patient_status_updater.rb @@ -46,18 +46,13 @@ def update_programme_statuses! batch = relation.includes( :attendance_record, + :consent_notifications, :consents, + :parents, :patient, :patient_locations, :triages, - :vaccination_records, - :parents, - :consent_notifications, - patient_locations: { - location: [ - { team_locations: { sessions: :session_programme_year_groups } } - ] - } + :vaccination_records ).to_a batch.each(&:assign) diff --git a/app/models/immunisation_import_row.rb b/app/models/immunisation_import_row.rb index b31d1a2a02..3dcbebde4a 100644 --- a/app/models/immunisation_import_row.rb +++ b/app/models/immunisation_import_row.rb @@ -553,6 +553,8 @@ def existing_patients(candidates: nil) def new_patient_attributes { address_postcode: patient_postcode&.to_postcode, + local_authority_mhclg_code: + LocalAuthority.for_postcode(patient_postcode&.to_postcode)&.mhclg_code, date_of_birth: patient_date_of_birth&.to_date, birth_academic_year: patient_date_of_birth&.to_date&.academic_year, family_name: patient_last_name.to_s, @@ -843,12 +845,18 @@ def validate_dose_sequence field = dose_sequence || combined_vaccination_and_dose_sequence + dose_sequence_examples = + (1..maximum_dose_sequence).to_a.to_sentence( + last_word_connector: " or ", + two_words_connector: " or " + ) + if dose_sequence.present? || parsed_vaccination_description_string&.dig(:dose_sequence).present? if dose_sequence_value.nil? errors.add( field.header, - "Enter a dose sequence number, for example, 1, 2 or 3." + "Enter a dose sequence number, for example, #{dose_sequence_examples}." ) elsif maximum_dose_sequence if dose_sequence_value < 1 @@ -873,7 +881,7 @@ def validate_dose_sequence else errors.add( field.header, - "Enter a dose sequence number, for example, 1, 2 or 3. The dose sequence number cannot be greater than 6." + "Enter a dose sequence number, for example, #{dose_sequence_examples}." ) end end diff --git a/app/models/location.rb b/app/models/location.rb index ba629a099b..a4c73f0296 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -15,6 +15,7 @@ # gias_year_groups :integer default([]), not null, is an Array # name :text not null # ods_code :string +# position :geography point, 4326 # site :string # status :integer default("unknown"), not null # systm_one_code :string @@ -247,9 +248,15 @@ def phase end def as_json - super.except("created_at", "systm_one_code", "updated_at").merge( + super.except( + "created_at", + "systm_one_code", + "updated_at", + "position" + ).merge( "is_attached_to_team" => - team_locations.any? { it.academic_year == AcademicYear.pending } + team_locations.any? { it.academic_year == AcademicYear.pending }, + "position" => position ? [position.x, position.y] : nil ) end diff --git a/app/models/notify_log_entry.rb b/app/models/notify_log_entry.rb index 58577c668a..fba7dcf3bf 100644 --- a/app/models/notify_log_entry.rb +++ b/app/models/notify_log_entry.rb @@ -7,7 +7,7 @@ # id :bigint not null, primary key # body :text # delivery_status :integer default("sending"), not null -# purpose :integer +# purpose :integer not null # recipient :string not null # subject :text # type :integer not null @@ -128,6 +128,7 @@ class NotifyLogEntry < ApplicationRecord vaccination_deleted: 13 } + validates :purpose, presence: true validates :recipient, presence: true validates :template_id, presence: true @@ -158,40 +159,6 @@ def title def programmes = notify_log_entry_programmes.map(&:programme) - def self.purpose_for_template_name(template_name_sym) - name = template_name_sym.to_s - - if name.include?("consent") && name.include?("request") - :consent_request - elsif name.include?("consent") && name.include?("reminder") - :consent_reminder - elsif name.include?("consent_confirmation") - :consent_confirmation - elsif name.include?("consent") && name.include?("warning") - :consent_warning - elsif name.include?("clinic") && name.include?("invitation") - :clinic_invitation - elsif name.include?("session_school_reminder") - :session_reminder - elsif name.include?("triage_vaccination_will_happen") - :triage_vaccination_will_happen - elsif name.include?("triage_vaccination_wont_happen") - :triage_vaccination_wont_happen - elsif name.include?("triage_vaccination_at_clinic") - :triage_vaccination_at_clinic - elsif name.include?("triage_delay_vaccination") - :triage_delay_vaccination - elsif name.include?("vaccination_administered") - :vaccination_administered - elsif name.include?("vaccination_already_had") - :vaccination_already_had - elsif name.include?("vaccination_not_administered") - :vaccination_not_administered - elsif name.include?("vaccination_deleted") - :vaccination_deleted - end - end - private def template_name diff --git a/app/validators/nhs_number_validator.rb b/app/validators/nhs_number_validator.rb index b62737497e..b5f8bd086a 100644 --- a/app/validators/nhs_number_validator.rb +++ b/app/validators/nhs_number_validator.rb @@ -27,7 +27,7 @@ def validate_each(record, attribute, value) digits .slice(0, 9) .each_with_index - .map { |digit, index| ((11 - (index + 1)) * digit) } + .map { |digit, index| (11 - (index + 1)) * digit } digits_sum_remainder = digits_multiplied_by_weighting_factor.sum % 11 diff --git a/app/views/notify_templates/email/clinic_initial_invitation.text.erb b/app/views/notify_templates/email/clinic_initial_invitation.text.erb index 81572eaad9..5fe52c49f8 100644 --- a/app/views/notify_templates/email/clinic_initial_invitation.text.erb +++ b/app/views/notify_templates/email/clinic_initial_invitation.text.erb @@ -1,5 +1,6 @@ --- template_id: "ceea5ff5-2250-4eb2-ab35-4e9e840b2a6f" +purpose: clinic_invitation template_name: clinic_initial_invitation subject: "<%= short_patient_name %> has still not had their <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/clinic_initial_invitation_rt5.text.erb b/app/views/notify_templates/email/clinic_initial_invitation_rt5.text.erb index 72fc5663c1..2d08e2597f 100644 --- a/app/views/notify_templates/email/clinic_initial_invitation_rt5.text.erb +++ b/app/views/notify_templates/email/clinic_initial_invitation_rt5.text.erb @@ -1,5 +1,6 @@ --- template_id: "17e63d67-53fc-4e9a-a533-74974412aac0" +purpose: clinic_invitation template_name: clinic_initial_invitation_rt5 subject: "<%= short_patient_name %> has still not had their <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/clinic_initial_invitation_ryg.text.erb b/app/views/notify_templates/email/clinic_initial_invitation_ryg.text.erb index e60bbbd017..87ee73b5f5 100644 --- a/app/views/notify_templates/email/clinic_initial_invitation_ryg.text.erb +++ b/app/views/notify_templates/email/clinic_initial_invitation_ryg.text.erb @@ -1,5 +1,6 @@ --- template_id: "5fe4fb4d-6f0a-4149-a80a-232bdfdf4f73" +purpose: clinic_invitation template_name: clinic_initial_invitation_ryg subject: "<%= short_patient_name %> has still not had their <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/clinic_subsequent_invitation.text.erb b/app/views/notify_templates/email/clinic_subsequent_invitation.text.erb index f19e3e0480..26848dc988 100644 --- a/app/views/notify_templates/email/clinic_subsequent_invitation.text.erb +++ b/app/views/notify_templates/email/clinic_subsequent_invitation.text.erb @@ -1,5 +1,6 @@ --- template_id: "a86a3b3f-a848-41d8-9a6f-d38174981388" +purpose: clinic_invitation template_name: clinic_subsequent_invitation subject: "Your child can still get their <%= vaccination %> at a clinic" --- diff --git a/app/views/notify_templates/email/clinic_subsequent_invitation_ryg.text.erb b/app/views/notify_templates/email/clinic_subsequent_invitation_ryg.text.erb index 8eb0964038..adfa236928 100644 --- a/app/views/notify_templates/email/clinic_subsequent_invitation_ryg.text.erb +++ b/app/views/notify_templates/email/clinic_subsequent_invitation_ryg.text.erb @@ -1,5 +1,6 @@ --- template_id: "eee59c1b-3af4-4ccd-8653-940887066390" +purpose: clinic_invitation template_name: clinic_subsequent_invitation_ryg subject: "Your child can still get their <%= vaccination %> at a clinic" --- diff --git a/app/views/notify_templates/email/consent_clinic_request.text.erb b/app/views/notify_templates/email/consent_clinic_request.text.erb index 2d41d74cec..d375f4bf1d 100644 --- a/app/views/notify_templates/email/consent_clinic_request.text.erb +++ b/app/views/notify_templates/email/consent_clinic_request.text.erb @@ -1,5 +1,6 @@ --- template_id: "14e88a09-4281-4257-9574-6afeaeb42715" +purpose: consent_request template_name: consent_clinic_request subject: "We still need consent for your child’s <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/consent_confirmation_clinic.text.erb b/app/views/notify_templates/email/consent_confirmation_clinic.text.erb index 7af9913f3a..d0725d6da0 100644 --- a/app/views/notify_templates/email/consent_confirmation_clinic.text.erb +++ b/app/views/notify_templates/email/consent_confirmation_clinic.text.erb @@ -1,5 +1,6 @@ --- template_id: "1d050527-9a6c-4513-86d4-6955b98ac7d9" +purpose: consent_confirmation template_name: consent_confirmation_clinic subject: "Your child’s <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/consent_confirmation_given.text.erb b/app/views/notify_templates/email/consent_confirmation_given.text.erb index ecc4bfc467..1a53114d84 100644 --- a/app/views/notify_templates/email/consent_confirmation_given.text.erb +++ b/app/views/notify_templates/email/consent_confirmation_given.text.erb @@ -1,5 +1,6 @@ --- template_id: "c6c8dbfc-b429-4468-bd0b-176e771b5a8e" +purpose: consent_confirmation template_name: consent_confirmation_given subject: "<%= vaccination_and_dates %> for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_confirmation_refused.text.erb b/app/views/notify_templates/email/consent_confirmation_refused.text.erb index 3c928b4287..ef43b49ce5 100644 --- a/app/views/notify_templates/email/consent_confirmation_refused.text.erb +++ b/app/views/notify_templates/email/consent_confirmation_refused.text.erb @@ -1,5 +1,6 @@ --- template_id: "5a676dac-3385-49e4-98c2-fc6b45b5a851" +purpose: consent_confirmation template_name: consent_confirmation_refused subject: "<%= vaccination_and_dates %> for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_confirmation_triage.text.erb b/app/views/notify_templates/email/consent_confirmation_triage.text.erb index 2cd8925736..41e97b4ab3 100644 --- a/app/views/notify_templates/email/consent_confirmation_triage.text.erb +++ b/app/views/notify_templates/email/consent_confirmation_triage.text.erb @@ -1,5 +1,6 @@ --- template_id: "35d621db-957b-4afb-9143-3e32398d2b87" +purpose: consent_confirmation template_name: consent_confirmation_triage subject: "<%= vaccination_and_dates %> for <%= short_patient_name %>" --- @@ -7,7 +8,7 @@ You’ve given consent for <%= full_and_preferred_patient_name %> to have their <%= consented_vaccine_methods_message %> -As you answered ‘yes’ to one or more of the health questions, we need to review your answers so we can decide what’s best for <%= short_patient_name %>. We’ll let you know once we’ve done this. +As you answered ‘yes’ to one or more of the health questions, we’ll review your answers so we can decide what’s best for <%= short_patient_name %>. We’ll contact you if we need to delay or cancel the vaccination. If you want to withdraw your consent in the meantime, please phone us. diff --git a/app/views/notify_templates/email/consent_school_reminder_doubles.text.erb b/app/views/notify_templates/email/consent_school_reminder_doubles.text.erb index 066d0a357f..7a82daa9bb 100644 --- a/app/views/notify_templates/email/consent_school_reminder_doubles.text.erb +++ b/app/views/notify_templates/email/consent_school_reminder_doubles.text.erb @@ -1,5 +1,6 @@ --- template_id: "3523d4b8-530b-42dd-8b9b-7fed8d1dfff1" +purpose: consent_reminder template_name: consent_school_reminder_doubles subject: "Please respond to our request for consent by <%= consent_deadline %>" --- diff --git a/app/views/notify_templates/email/consent_school_reminder_flu.text.erb b/app/views/notify_templates/email/consent_school_reminder_flu.text.erb index a6409537d4..5465298e3f 100644 --- a/app/views/notify_templates/email/consent_school_reminder_flu.text.erb +++ b/app/views/notify_templates/email/consent_school_reminder_flu.text.erb @@ -1,5 +1,6 @@ --- template_id: "7f85a5b4-5240-4ae9-94f7-43913852943c" +purpose: consent_reminder template_name: consent_school_reminder_flu subject: "Please respond to our request for consent by <%= consent_deadline %>" --- diff --git a/app/views/notify_templates/email/consent_school_reminder_hpv.text.erb b/app/views/notify_templates/email/consent_school_reminder_hpv.text.erb index 778be7f312..38bc5ed757 100644 --- a/app/views/notify_templates/email/consent_school_reminder_hpv.text.erb +++ b/app/views/notify_templates/email/consent_school_reminder_hpv.text.erb @@ -1,5 +1,6 @@ --- template_id: "0d78bff0-9dde-4192-8cf8-10e83486b54f" +purpose: consent_reminder template_name: consent_school_reminder_hpv subject: "Please respond to our request for consent by <%= consent_deadline %>" --- diff --git a/app/views/notify_templates/email/consent_school_reminder_mmr.text.erb b/app/views/notify_templates/email/consent_school_reminder_mmr.text.erb index 926d993851..59ce4c2269 100644 --- a/app/views/notify_templates/email/consent_school_reminder_mmr.text.erb +++ b/app/views/notify_templates/email/consent_school_reminder_mmr.text.erb @@ -1,5 +1,6 @@ --- template_id: "5462c441-81c0-4ac0-821f-713b4178f8ba" +purpose: consent_reminder template_name: consent_school_reminder_mmr subject: "Please respond to our request for consent by <%= consent_deadline %>" --- diff --git a/app/views/notify_templates/email/consent_school_reminder_mmrv.text.erb b/app/views/notify_templates/email/consent_school_reminder_mmrv.text.erb index ed131fe680..cc844d8e91 100644 --- a/app/views/notify_templates/email/consent_school_reminder_mmrv.text.erb +++ b/app/views/notify_templates/email/consent_school_reminder_mmrv.text.erb @@ -1,5 +1,6 @@ --- template_id: "fe47875a-a0a6-40d9-bd41-a411ebb31cff" +purpose: consent_reminder template_name: consent_school_reminder_mmrv subject: "Please respond to our request for consent by <%= consent_deadline %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_doubles.text.erb b/app/views/notify_templates/email/consent_school_request_doubles.text.erb index a1b1e3ae8f..5be305876b 100644 --- a/app/views/notify_templates/email/consent_school_request_doubles.text.erb +++ b/app/views/notify_templates/email/consent_school_request_doubles.text.erb @@ -1,5 +1,6 @@ --- template_id: "9b1a015d-6caa-47c5-a223-f72377586602" +purpose: consent_request template_name: consent_school_request_doubles subject: "MenACWY and Td/IPV vaccinations for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_flu.text.erb b/app/views/notify_templates/email/consent_school_request_flu.text.erb index db777d6be1..390a6a383a 100644 --- a/app/views/notify_templates/email/consent_school_request_flu.text.erb +++ b/app/views/notify_templates/email/consent_school_request_flu.text.erb @@ -1,5 +1,6 @@ --- template_id: "017853bc-2b35-4aff-99b1-193e514613a0" +purpose: consent_request template_name: consent_school_request_flu subject: "Annual flu vaccination for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_hpv.text.erb b/app/views/notify_templates/email/consent_school_request_hpv.text.erb index fa6796b1c2..e129c0f74d 100644 --- a/app/views/notify_templates/email/consent_school_request_hpv.text.erb +++ b/app/views/notify_templates/email/consent_school_request_hpv.text.erb @@ -1,5 +1,6 @@ --- template_id: "7b9bb010-0742-460a-ae25-1922355b6776" +purpose: consent_request template_name: consent_school_request_hpv subject: "HPV vaccination for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_mmr.text.erb b/app/views/notify_templates/email/consent_school_request_mmr.text.erb index 5083e80572..9e1a7a5e75 100644 --- a/app/views/notify_templates/email/consent_school_request_mmr.text.erb +++ b/app/views/notify_templates/email/consent_school_request_mmr.text.erb @@ -1,5 +1,6 @@ --- template_id: "7e86e688-ceca-4dcc-a1cf-19cb559d38a8" +purpose: consent_request template_name: consent_school_request_mmr subject: "MMR (measles, mumps and rubella) catch-up vaccination for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_mmr_outbreak.text.erb b/app/views/notify_templates/email/consent_school_request_mmr_outbreak.text.erb index ccc0df984a..d399613fb4 100644 --- a/app/views/notify_templates/email/consent_school_request_mmr_outbreak.text.erb +++ b/app/views/notify_templates/email/consent_school_request_mmr_outbreak.text.erb @@ -1,5 +1,6 @@ --- template_id: "517b02ee-2b1c-493e-bec2-1ee39f73dbae" +purpose: consent_request template_name: consent_school_request_mmr_outbreak subject: "Cases of measles are high – make sure <%= short_patient_name %> is vaccinated" --- diff --git a/app/views/notify_templates/email/consent_school_request_mmrv.text.erb b/app/views/notify_templates/email/consent_school_request_mmrv.text.erb index 7fdd2431cf..e4dd516d53 100644 --- a/app/views/notify_templates/email/consent_school_request_mmrv.text.erb +++ b/app/views/notify_templates/email/consent_school_request_mmrv.text.erb @@ -1,5 +1,6 @@ --- template_id: "fe194b88-5692-49a2-ab14-648e8ce2af63" +purpose: consent_request template_name: consent_school_request_mmrv subject: "MMRV (measles, mumps, rubella and varicella) catch-up vaccination for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/consent_school_request_mmrv_outbreak.text.erb b/app/views/notify_templates/email/consent_school_request_mmrv_outbreak.text.erb index a4f3cada83..1df592af8b 100644 --- a/app/views/notify_templates/email/consent_school_request_mmrv_outbreak.text.erb +++ b/app/views/notify_templates/email/consent_school_request_mmrv_outbreak.text.erb @@ -1,5 +1,6 @@ --- template_id: "abe274c2-cd29-4099-b3ff-0e5ed710e532" +purpose: consent_request template_name: consent_school_request_mmrv_outbreak subject: "Cases of measles are high – make sure <%= short_patient_name %> is vaccinated" --- diff --git a/app/views/notify_templates/email/consent_unknown_contact_details_warning.text.erb b/app/views/notify_templates/email/consent_unknown_contact_details_warning.text.erb index 672179e6de..314cd83523 100644 --- a/app/views/notify_templates/email/consent_unknown_contact_details_warning.text.erb +++ b/app/views/notify_templates/email/consent_unknown_contact_details_warning.text.erb @@ -1,5 +1,6 @@ --- template_id: "6d746839-a20e-4d50-8a1d-6f3900ff69b2" +purpose: consent_warning template_name: consent_unknown_contact_details_warning subject: "Different contact details in <%= vaccination %> consent response" --- diff --git a/app/views/notify_templates/email/session_school_reminder.text.erb b/app/views/notify_templates/email/session_school_reminder.text.erb index e15d32f2d6..fbf3e6721c 100644 --- a/app/views/notify_templates/email/session_school_reminder.text.erb +++ b/app/views/notify_templates/email/session_school_reminder.text.erb @@ -1,5 +1,6 @@ --- template_id: "8b8a9566-bb03-4b3c-8abc-5bd5a4b8797d" +purpose: session_reminder template_name: session_school_reminder subject: "<%= full_and_preferred_patient_name %> may get their <%= vaccination %> on <%= next_session_dates_or %>" --- diff --git a/app/views/notify_templates/email/triage_delay_vaccination.text.erb b/app/views/notify_templates/email/triage_delay_vaccination.text.erb index c856ef6a62..aee7e11e24 100644 --- a/app/views/notify_templates/email/triage_delay_vaccination.text.erb +++ b/app/views/notify_templates/email/triage_delay_vaccination.text.erb @@ -1,5 +1,6 @@ --- template_id: "0e37d12a-5469-4ad2-aa09-83e0ef56e03e" +purpose: triage_delay_vaccination template_name: triage_delay_vaccination subject: "We are delaying your child’s <%= vaccination %>" --- diff --git a/app/views/notify_templates/email/triage_vaccination_at_clinic.text.erb b/app/views/notify_templates/email/triage_vaccination_at_clinic.text.erb index 97b949bf85..06d5aaf550 100644 --- a/app/views/notify_templates/email/triage_vaccination_at_clinic.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_at_clinic.text.erb @@ -1,5 +1,6 @@ --- template_id: "3c7461bd-e3cf-4ff9-9053-b4e87490aa45" +purpose: triage_vaccination_at_clinic template_name: triage_vaccination_at_clinic subject: "Please book a clinic appointment for your child’s vaccination" --- diff --git a/app/views/notify_templates/email/triage_vaccination_at_clinic_rt5.text.erb b/app/views/notify_templates/email/triage_vaccination_at_clinic_rt5.text.erb index f7aaa8069b..8edebc6051 100644 --- a/app/views/notify_templates/email/triage_vaccination_at_clinic_rt5.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_at_clinic_rt5.text.erb @@ -1,5 +1,6 @@ --- template_id: "5cacefcd-44f2-43ff-8fc0-008890406504" +purpose: triage_vaccination_at_clinic template_name: triage_vaccination_at_clinic_rt5 subject: "Vaccination at a community clinic" --- diff --git a/app/views/notify_templates/email/triage_vaccination_at_clinic_ryg.text.erb b/app/views/notify_templates/email/triage_vaccination_at_clinic_ryg.text.erb index f1bbe9f4f8..124463a616 100644 --- a/app/views/notify_templates/email/triage_vaccination_at_clinic_ryg.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_at_clinic_ryg.text.erb @@ -1,5 +1,6 @@ --- template_id: "9faef718-bd76-4c30-93ea-fbe8584388a6" +purpose: triage_vaccination_at_clinic template_name: triage_vaccination_at_clinic_ryg subject: "Please book a clinic appointment for your child’s vaccination" --- diff --git a/app/views/notify_templates/email/triage_vaccination_will_happen.text.erb b/app/views/notify_templates/email/triage_vaccination_will_happen.text.erb index 7f532c65e1..1614a5baa2 100644 --- a/app/views/notify_templates/email/triage_vaccination_will_happen.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_will_happen.text.erb @@ -1,5 +1,6 @@ --- template_id: "279c517c-4c52-4a69-96cb-31355bfa4e21" +purpose: triage_vaccination_will_happen template_name: triage_vaccination_will_happen subject: "Your child can have their <%= vaccination %> on <%= next_session_dates_or %>" --- diff --git a/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb b/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb index 679894fc13..6eaaa0a260 100644 --- a/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb @@ -1,5 +1,6 @@ --- template_id: "6fd910fd-120c-4e58-9ef3-15ffc5bd6edc" +purpose: triage_vaccination_will_happen template_name: triage_vaccination_will_happen_mmr_second_dose subject: "<%= patient.short_name %> needs another dose of the <%= programme_name_for_parents(mmr_programme.variant_for(patient:)) %> vaccination" --- diff --git a/app/views/notify_templates/email/triage_vaccination_wont_happen.text.erb b/app/views/notify_templates/email/triage_vaccination_wont_happen.text.erb index c6bb6090bc..9cb2f64083 100644 --- a/app/views/notify_templates/email/triage_vaccination_wont_happen.text.erb +++ b/app/views/notify_templates/email/triage_vaccination_wont_happen.text.erb @@ -1,5 +1,6 @@ --- template_id: "d1faf47e-ccc3-4481-975b-1ec34211a21f" +purpose: triage_vaccination_wont_happen template_name: triage_vaccination_wont_happen subject: "Your child will not have their <%= vaccination %> in school" --- diff --git a/app/views/notify_templates/email/vaccination_administered.text.erb b/app/views/notify_templates/email/vaccination_administered.text.erb index c49abe9dcc..475c2ca696 100644 --- a/app/views/notify_templates/email/vaccination_administered.text.erb +++ b/app/views/notify_templates/email/vaccination_administered.text.erb @@ -1,5 +1,6 @@ --- template_id: "bd51556d-bccc-469c-a822-0b88f26efd10" +purpose: vaccination_administered template_name: vaccination_administered subject: "Your child had their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination <%= vaccination_record_today_or_date(vaccination_record) %>" --- diff --git a/app/views/notify_templates/email/vaccination_already_had.text.erb b/app/views/notify_templates/email/vaccination_already_had.text.erb index c42c5b541f..7d4cb9ef1f 100644 --- a/app/views/notify_templates/email/vaccination_already_had.text.erb +++ b/app/views/notify_templates/email/vaccination_already_had.text.erb @@ -1,5 +1,6 @@ --- template_id: "e37fe0a2-7584-4c25-983a-8f5a11c818a1" +purpose: vaccination_already_had template_name: vaccination_already_had subject: "Cancelled <%= programme_name_for_parents(vaccination_record.programme) %> vaccination appointment for <%= short_patient_name %>" --- diff --git a/app/views/notify_templates/email/vaccination_deleted.text.erb b/app/views/notify_templates/email/vaccination_deleted.text.erb index 33514dd0e5..2e4073fcc2 100644 --- a/app/views/notify_templates/email/vaccination_deleted.text.erb +++ b/app/views/notify_templates/email/vaccination_deleted.text.erb @@ -1,5 +1,6 @@ --- template_id: "1caf1459-abc9-4944-b8c0-deba906ea005" +purpose: vaccination_deleted template_name: vaccination_deleted subject: "Our last email to you was inaccurate" --- diff --git a/app/views/notify_templates/email/vaccination_not_administered.text.erb b/app/views/notify_templates/email/vaccination_not_administered.text.erb index f08c934157..0794c31c97 100644 --- a/app/views/notify_templates/email/vaccination_not_administered.text.erb +++ b/app/views/notify_templates/email/vaccination_not_administered.text.erb @@ -1,5 +1,6 @@ --- template_id: "130fe52a-014a-45dd-9f53-8e65c1b8bb79" +purpose: vaccination_not_administered template_name: vaccination_not_administered subject: "Your child did not have their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination today" --- diff --git a/app/views/notify_templates/sms/clinic_initial_invitation.text.erb b/app/views/notify_templates/sms/clinic_initial_invitation.text.erb index 450ea1bba0..07b48adb04 100644 --- a/app/views/notify_templates/sms/clinic_initial_invitation.text.erb +++ b/app/views/notify_templates/sms/clinic_initial_invitation.text.erb @@ -1,5 +1,6 @@ --- template_id: "790c9c72-729a-40d6-b44d-d480e38f0990" +purpose: clinic_invitation template_name: clinic_initial_invitation --- Our records show that <%= full_and_preferred_patient_name %> has not had their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/clinic_initial_invitation_rt5.text.erb b/app/views/notify_templates/sms/clinic_initial_invitation_rt5.text.erb index aebdc5b75e..8f3fe18bb1 100644 --- a/app/views/notify_templates/sms/clinic_initial_invitation_rt5.text.erb +++ b/app/views/notify_templates/sms/clinic_initial_invitation_rt5.text.erb @@ -1,5 +1,6 @@ --- template_id: "7be79abb-7295-4e6f-8cfb-9597bfad2f56" +purpose: clinic_invitation template_name: clinic_initial_invitation_rt5 --- Our records show that <%= full_and_preferred_patient_name %> has not had their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/clinic_initial_invitation_ryg.text.erb b/app/views/notify_templates/sms/clinic_initial_invitation_ryg.text.erb index aa4c06e33d..ec29b160ed 100644 --- a/app/views/notify_templates/sms/clinic_initial_invitation_ryg.text.erb +++ b/app/views/notify_templates/sms/clinic_initial_invitation_ryg.text.erb @@ -1,5 +1,6 @@ --- template_id: "8ef5712f-bb7f-4911-8f3b-19df6f8a7179" +purpose: clinic_invitation template_name: clinic_initial_invitation_ryg --- Our records show that <%= full_and_preferred_patient_name %> has not had their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/clinic_subsequent_invitation.text.erb b/app/views/notify_templates/sms/clinic_subsequent_invitation.text.erb index 4e63482323..f8c2f4a1d5 100644 --- a/app/views/notify_templates/sms/clinic_subsequent_invitation.text.erb +++ b/app/views/notify_templates/sms/clinic_subsequent_invitation.text.erb @@ -1,5 +1,6 @@ --- template_id: "ce7a6a1b-465e-4be4-b9e0-47ddb64f3adb" +purpose: clinic_invitation template_name: clinic_subsequent_invitation --- It's not too late for <%= full_and_preferred_patient_name %> to get their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/clinic_subsequent_invitation_ryg.text.erb b/app/views/notify_templates/sms/clinic_subsequent_invitation_ryg.text.erb index 71756919fb..227bb01af1 100644 --- a/app/views/notify_templates/sms/clinic_subsequent_invitation_ryg.text.erb +++ b/app/views/notify_templates/sms/clinic_subsequent_invitation_ryg.text.erb @@ -1,5 +1,6 @@ --- template_id: "018f146d-e7b7-4b63-ae26-bb07ca6fe2f9" +purpose: clinic_invitation template_name: clinic_subsequent_invitation_ryg --- It's not too late for <%= full_and_preferred_patient_name %> to get their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/consent_clinic_request.text.erb b/app/views/notify_templates/sms/consent_clinic_request.text.erb index f8f28341d9..dad5390492 100644 --- a/app/views/notify_templates/sms/consent_clinic_request.text.erb +++ b/app/views/notify_templates/sms/consent_clinic_request.text.erb @@ -1,5 +1,6 @@ --- template_id: "03a0d572-ca5b-417e-87c3-838872a9eabc" +purpose: consent_request template_name: consent_clinic_request --- You recently booked a clinic appointment for <%= full_and_preferred_patient_name %>. diff --git a/app/views/notify_templates/sms/consent_confirmation_given.text.erb b/app/views/notify_templates/sms/consent_confirmation_given.text.erb index 7aa1320d34..e45f16d046 100644 --- a/app/views/notify_templates/sms/consent_confirmation_given.text.erb +++ b/app/views/notify_templates/sms/consent_confirmation_given.text.erb @@ -1,5 +1,6 @@ --- template_id: "8eb8d05e-b8d8-4bf9-8a38-c009ae989a4e" +purpose: consent_confirmation template_name: consent_confirmation_given --- You've given consent for <%= short_patient_name %> to get their <%= vaccination_and_dates %>. diff --git a/app/views/notify_templates/sms/consent_confirmation_refused.text.erb b/app/views/notify_templates/sms/consent_confirmation_refused.text.erb index b7fc4a1f1d..db252dc9c5 100644 --- a/app/views/notify_templates/sms/consent_confirmation_refused.text.erb +++ b/app/views/notify_templates/sms/consent_confirmation_refused.text.erb @@ -1,5 +1,6 @@ --- template_id: "234b7479-1968-4f57-a6bf-20e402c8da39" +purpose: consent_confirmation template_name: consent_confirmation_refused --- You have told us you do not want <%= short_patient_name %> to have their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/consent_school_reminder.text.erb b/app/views/notify_templates/sms/consent_school_reminder.text.erb index 01129a2dce..9ee9b1572d 100644 --- a/app/views/notify_templates/sms/consent_school_reminder.text.erb +++ b/app/views/notify_templates/sms/consent_school_reminder.text.erb @@ -1,5 +1,6 @@ --- template_id: "26029539-60e4-416b-a3a8-40b82c2babc1" +purpose: consent_reminder template_name: consent_school_reminder --- We recently asked you to give or refuse consent for <%= short_patient_name %> to have their <%= vaccination %>. diff --git a/app/views/notify_templates/sms/consent_school_request.text.erb b/app/views/notify_templates/sms/consent_school_request.text.erb index 8684a8a528..107d0b2b37 100644 --- a/app/views/notify_templates/sms/consent_school_request.text.erb +++ b/app/views/notify_templates/sms/consent_school_request.text.erb @@ -1,5 +1,6 @@ --- template_id: "2d3e2370-7faa-4798-b7ae-607692a85059" +purpose: consent_request template_name: consent_school_request --- Give or refuse consent for <%= short_patient_name %> to have their <%= vaccination %>: diff --git a/app/views/notify_templates/sms/consent_school_request_mmr.text.erb b/app/views/notify_templates/sms/consent_school_request_mmr.text.erb index 2a2b60a34b..19b1a68f63 100644 --- a/app/views/notify_templates/sms/consent_school_request_mmr.text.erb +++ b/app/views/notify_templates/sms/consent_school_request_mmr.text.erb @@ -1,5 +1,6 @@ --- template_id: "710bf0f1-3916-4f90-a07c-d6999090b230" +purpose: consent_request template_name: consent_school_request_mmr --- <% if outbreak? %> diff --git a/app/views/notify_templates/sms/consent_unknown_contact_details_warning.text.erb b/app/views/notify_templates/sms/consent_unknown_contact_details_warning.text.erb index 62cace6218..c5a641d92a 100644 --- a/app/views/notify_templates/sms/consent_unknown_contact_details_warning.text.erb +++ b/app/views/notify_templates/sms/consent_unknown_contact_details_warning.text.erb @@ -1,5 +1,6 @@ --- template_id: "1fd4620d-1c96-4af1-b047-ed13a90b0f44" +purpose: consent_warning template_name: consent_unknown_contact_details_warning --- We got a vaccination consent response for <%= short_patient_name %>. If you think it came from someone without parental responsibility, contact us <%= subteam_phone %>. If you sent it, you do not need to do anything. diff --git a/app/views/notify_templates/sms/session_school_reminder.text.erb b/app/views/notify_templates/sms/session_school_reminder.text.erb index 76279a33b2..9509d87246 100644 --- a/app/views/notify_templates/sms/session_school_reminder.text.erb +++ b/app/views/notify_templates/sms/session_school_reminder.text.erb @@ -1,5 +1,6 @@ --- template_id: "cc4a7f89-d260-461c-80f0-7e6e9af75e7a" +purpose: session_reminder template_name: session_school_reminder --- <%= short_patient_name %> may get their <%= vaccination %> at school on <%= next_session_date %>. diff --git a/app/views/notify_templates/sms/vaccination_administered.text.erb b/app/views/notify_templates/sms/vaccination_administered.text.erb index 467e8e913c..d52047b965 100644 --- a/app/views/notify_templates/sms/vaccination_administered.text.erb +++ b/app/views/notify_templates/sms/vaccination_administered.text.erb @@ -1,5 +1,6 @@ --- template_id: "395a3ea1-df07-4dd6-8af1-64cc597ef383" +purpose: vaccination_administered template_name: vaccination_administered --- <%= short_patient_name %> had their <%= vaccination_and_method %> today. They might have some of the following side effects: diff --git a/app/views/notify_templates/sms/vaccination_already_had.text.erb b/app/views/notify_templates/sms/vaccination_already_had.text.erb index 1d9659d476..6b2e76dbe7 100644 --- a/app/views/notify_templates/sms/vaccination_already_had.text.erb +++ b/app/views/notify_templates/sms/vaccination_already_had.text.erb @@ -1,5 +1,6 @@ --- template_id: "fab1e355-bde1-47d5-835c-103bfd232b93" +purpose: vaccination_already_had template_name: vaccination_already_had --- We are cancelling <%= short_patient_name %>'s <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at school diff --git a/app/views/notify_templates/sms/vaccination_not_administered.text.erb b/app/views/notify_templates/sms/vaccination_not_administered.text.erb index 9f5b86c0fd..84bf71237b 100644 --- a/app/views/notify_templates/sms/vaccination_not_administered.text.erb +++ b/app/views/notify_templates/sms/vaccination_not_administered.text.erb @@ -1,5 +1,6 @@ --- template_id: "aae061e0-b847-4d4c-a87a-12508f95a302" +purpose: vaccination_not_administered template_name: vaccination_not_administered --- <%= short_patient_name %> did not have their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at school today. This was because <%= reason_did_not_vaccinate %>. diff --git a/app/views/patients/show.html.erb b/app/views/patients/show.html.erb index 708f5357f4..d9567fe1f7 100644 --- a/app/views/patients/show.html.erb +++ b/app/views/patients/show.html.erb @@ -38,3 +38,5 @@ ) %> <% end %> <% end %> + +<%= render AppPatientActivityComponent.new(@patient, team: current_team) %> diff --git a/config/application.rb b/config/application.rb index 7e57bf0fca..f1962b201a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,7 +41,7 @@ class Application < Rails::Application dbname = db_config["dbname"] ENV[ "DATABASE_URL" - ] = "postgres://#{username}:#{password}@#{host}:#{port}/#{dbname}" + ] = "postgis://#{username}:#{password}@#{host}:#{port}/#{dbname}" elsif ENV["DB_CREDENTIALS"].present? # for environment which uses RDS aurora managed credentials only the the username # and password is automatically set. The environment variable is then DB_CREDENTIALS @@ -53,7 +53,7 @@ class Application < Rails::Application port = ENV.fetch("DB_PORT", 5432) ENV[ "DATABASE_URL" - ] = "postgres://#{username}:#{password}@#{host}:#{port}/#{dbname}" + ] = "postgis://#{username}:#{password}@#{host}:#{port}/#{dbname}" end # Configuration for the application, engines, and railties goes here. diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 077d32b6bd..7a25c46880 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--QdRBn1/NPtPNGMHb--sD6DsVLuhIq3ie3oiDi41A== \ No newline at end of file +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--L2A6wZg08FovwmSC--c4zFln30g74CHVAzttQ+Gg== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index d127099166..bcd21762e3 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -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--phWJb4+t2Rx6FelG--7d3+ecnoHZtuZdq3kyM4Cg== \ No newline at end of file +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--7ZOF9aN+qHT+0z8Y--g4vO+C7T8p0HHQovB6BftA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index dd1415ab53..59c4a3b4bf 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,5 +1,5 @@ default: &default - adapter: postgresql + adapter: postgis encoding: unicode pool: <%= 5 * ENV.fetch("RAILS_MAX_THREADS", 5).to_i %> @@ -18,8 +18,7 @@ end_to_end: test: <<: *default database: manage_vaccinations_test<%= ENV['TEST_ENV_NUMBER'] %> - # Needed by CI server - host: <%= ENV.fetch("DATABASE_HOST", "localhost") %> + host: <%= ENV.fetch("DATABASE_HOST", "") %> username: <%= ENV.fetch("DATABASE_USER", "") %> password: <%= ENV.fetch("DATABASE_PASSWORD", "") %> staging: diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index f0899e9a52..4bd625a831 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -22,6 +22,7 @@ inflect.acronym "DfE" inflect.acronym "FHIR" inflect.acronym "GIAS" + inflect.acronym "GIS" inflect.acronym "GP" inflect.acronym "HCA" inflect.acronym "JWKS" diff --git a/config/locales/en.yml b/config/locales/en.yml index cda81162f7..1c046e9f31 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -216,6 +216,11 @@ en: attributes: apply_changes: inclusion: Choose which record to keep + notify_template: + attributes: + purpose: + blank: Add a purpose to the template frontmatter + inclusion: Use a purpose from the list of known purposes in NotifyLogEntry onboarding: attributes: clinics: @@ -803,6 +808,8 @@ en: blank: Enter a template ID type: inclusion: Choose a type + purpose: + blank: Add a purpose to the template frontmatter offline_password: attributes: password: diff --git a/config/routes.rb b/config/routes.rb index a1e0114f7c..4e72c89584 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -116,6 +116,8 @@ resources :teams, only: :destroy, param: :workgroup post "/onboard", to: "onboard#create" get "refresh-reporting", to: "reporting_refresh#create" + post "vaccinations-search-in-nhs", + to: "vaccinations_search_in_nhs#create" end end diff --git a/config/settings.yml b/config/settings.yml index 9d20599200..f90dbca8c5 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -60,6 +60,10 @@ reporting_api: careplus: base_url: https://careplus.syhapp.thirdparty.nhs.uk +ordnance_survey: + api_key: <%= Rails.application.credentials.ordnance_survey&.api_key %> + secret_key: <%= Rails.application.credentials.ordnance_survey&.secret_key %> + # Set a value to override the default values set in config/initializers/devise.rb devise: timeout_in_seconds: diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 135072c8fa..05a19f6b6d 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -12,78 +12,65 @@ - immunisations_api_sync - immunisations_api_search - third_party_data_imports + - careplus - metrics - default :scheduler: :schedule: - enqueue_vaccinations_search_in_nhs_job: + EnqueueVaccinationsSearchInNHSJob: cron: "0 6 * * *" - class: EnqueueVaccinationsSearchInNHSJob description: Find upcoming sessions and enqueue jobs to find vaccinations for patients in them - gias_import: + GIASImportJob: cron: "30 1 * * *" - class: GIASImportJob description: Download and import GIAS data - invalidate_self_consents: + InvalidateSelfConsentsJob: cron: "0 2 * * *" - class: InvalidateSelfConsentsJob description: Invalidate all self-consents and associated triage for the previous day - metrics_export_school_moves_count: + Metrics::ExportSchoolMovesCountJob: every: 30m - class: Metrics::ExportSchoolMovesCountJob description: Export school moves count metric - patients_aged_out_of_school: + EnqueuePatientsAgedOutOfSchoolsJob: cron: "0 5 * * *" - class: EnqueuePatientsAgedOutOfSchoolsJob description: Moves patients who have aged out of their school to unknown school - patients_clear_registration: + PatientsClearRegistrationJob: cron: "15 5 * * *" - class: PatientsClearRegistrationJob description: Clears the registration of patients for the previous academic year - patients_refused_consent_already_vaccinated: + PatientsRefusedConsentAlreadyVaccinatedJob: cron: "30 5 * * *" - class: PatientsRefusedConsentAlreadyVaccinatedJob description: >- Record already vaccinated for patients who refused consent in the previous academic year for that reason - refresh_reporting_data: + ReportingAPI::RefreshJob: cron: "0 * * * *" - class: ReportingAPI::RefreshJob description: Refresh the reporting API materialized view data - remove_import_csv: + RemoveImportCSVJob: cron: "0 1 * * *" - class: RemoveImportCSVJob description: Remove CSV data from old cohort and immunisation imports - school_consent_requests: + EnqueueSchoolConsentRequestsJob: cron: "0 16 * * *" - class: EnqueueSchoolConsentRequestsJob description: Send school consent request emails to parents for each session - school_consent_reminders: + EnqueueSchoolConsentRemindersJob: cron: "15 16 * * *" - class: EnqueueSchoolConsentRemindersJob description: Send school consent reminder emails to parents for each session - school_session_reminders: + EnqueueSchoolSessionRemindersJob: cron: "0 9 * * *" - class: EnqueueSchoolSessionRemindersJob description: Send school session reminder emails to parents - patient_status_updater: + PatientStatusUpdaterJob: cron: "0 3 * * *" - class: PatientStatusUpdaterJob description: Updates the status of all patients - trim_active_record_sessions: + TrimActiveRecordSessionsJob: cron: "0 2 * * *" - class: TrimActiveRecordSessionsJob description: Remove ActiveRecord sessions older than 30 days - update_patients_from_pds: + EnqueueUpdatePatientsFromPDSJob: cron: "0 0,6,12,18 * * *" - class: EnqueueUpdatePatientsFromPDSJob description: Keep patient details up to date with PDS - process_unmatched_consent_forms: + EnqueueProcessUnmatchedConsentFormsJob: cron: "0 4 * * *" - class: EnqueueProcessUnmatchedConsentFormsJob description: Re-process unmatched consent forms to attempt automatic matching - vaccination_confirmations: + SendVaccinationConfirmationsJob: cron: "0 13,16,19 * * *" - class: SendVaccinationConfirmationsJob description: Send vaccination confirmation emails to parents + EnqueueAutomatedCareplusReportsJob: + cron: "30 2 * * *" + description: Enqueue automated CarePlus reports for teams with CarePlus configured diff --git a/db/data/20260305165603_backfill_purpose_for_notify_log_entries.rb b/db/data/20260305165603_backfill_purpose_for_notify_log_entries.rb index dbeda4201a..203214ade6 100644 --- a/db/data/20260305165603_backfill_purpose_for_notify_log_entries.rb +++ b/db/data/20260305165603_backfill_purpose_for_notify_log_entries.rb @@ -21,7 +21,7 @@ def up template_name = NotifyTemplate.find_by_id(template_id, channel: type.to_sym)&.name next unless template_name - purpose = NotifyLogEntry.purpose_for_template_name(template_name) + purpose = purpose_for_template_name(template_name) next unless purpose updated_count = NotifyLogEntry @@ -55,4 +55,42 @@ def up def down raise ActiveRecord::IrreversibleMigration end + + private + + # Inlined from NotifyLogEntry.purpose_for_template_name (removed in MAV-6739) + # so this historical migration stays runnable if replayed. + def purpose_for_template_name(template_name_sym) + name = template_name_sym.to_s + + if name.include?("consent") && name.include?("request") + :consent_request + elsif name.include?("consent") && name.include?("reminder") + :consent_reminder + elsif name.include?("consent_confirmation") + :consent_confirmation + elsif name.include?("consent") && name.include?("warning") + :consent_warning + elsif name.include?("clinic") && name.include?("invitation") + :clinic_invitation + elsif name.include?("session_school_reminder") + :session_reminder + elsif name.include?("triage_vaccination_will_happen") + :triage_vaccination_will_happen + elsif name.include?("triage_vaccination_wont_happen") + :triage_vaccination_wont_happen + elsif name.include?("triage_vaccination_at_clinic") + :triage_vaccination_at_clinic + elsif name.include?("triage_delay_vaccination") + :triage_delay_vaccination + elsif name.include?("vaccination_administered") + :vaccination_administered + elsif name.include?("vaccination_already_had") + :vaccination_already_had + elsif name.include?("vaccination_not_administered") + :vaccination_not_administered + elsif name.include?("vaccination_deleted") + :vaccination_deleted + end + end end diff --git a/db/data/20260422152925_unset_local_authority_mhclg_code_from_pending_changes.rb b/db/data/20260422152925_unset_local_authority_mhclg_code_from_pending_changes.rb new file mode 100644 index 0000000000..3f3b2664e6 --- /dev/null +++ b/db/data/20260422152925_unset_local_authority_mhclg_code_from_pending_changes.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class UnsetLocalAuthorityMhclgCodeFromPendingChanges < ActiveRecord::Migration[8.1] + def up + count = Patient + .where("pending_changes->>'address_postcode' IS NULL") + .where("pending_changes->>'local_authority_mhclg_code' IS NOT NULL") + .update_all <<~SQL + local_authority_mhclg_code = pending_changes->>'local_authority_mhclg_code', + pending_changes = pending_changes - 'local_authority_mhclg_code' + SQL + + Rails.logger.debug "Updated #{count} patients" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/lib/tasks/local_authority.rake b/db/data/20260422155320_backfill_local_authority_mhclg_code.rb similarity index 54% rename from lib/tasks/local_authority.rake rename to db/data/20260422155320_backfill_local_authority_mhclg_code.rb index 1bcef8d644..09ab5ed677 100644 --- a/lib/tasks/local_authority.rake +++ b/db/data/20260422155320_backfill_local_authority_mhclg_code.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -namespace :local_authority do - desc "Backfill local_authority_mhclg_code on patients (one-time)" - task backfill_patients: :environment do +class BackfillLocalAuthorityMhclgCode < ActiveRecord::Migration[8.1] + def up scope = Patient.where(local_authority_mhclg_code: nil) total = scope.count - puts "Checking #{total} patients..." + Rails.logger.debug "Checking #{total} patients..." processed = 0 updated = 0 @@ -26,13 +25,17 @@ end if (processed % 10_000).zero? - puts "Processed #{processed}/#{total} (updated: #{updated})" + Rails.logger.debug "Processed #{processed}/#{total} (updated: #{updated})" end end - puts "Done. Processed #{processed} patients." - puts " Updated: #{updated}" - puts " Skipped (no postcode): #{skipped_no_postcode}" - puts " Skipped (no LA match): #{skipped_no_match}" + Rails.logger.debug "Done. Processed #{processed} patients." + Rails.logger.debug " Updated: #{updated}" + Rails.logger.debug " Skipped (no postcode): #{skipped_no_postcode}" + Rails.logger.debug " Skipped (no LA match): #{skipped_no_match}" + end + + def down + raise ActiveRecord::IrreversibleMigration end end diff --git a/db/data_schema.rb b/db/data_schema.rb index 74db247d39..2635bc89c3 100644 --- a/db/data_schema.rb +++ b/db/data_schema.rb @@ -1 +1 @@ -DataMigrate::Data.define(version: 2026_04_21_140000) +DataMigrate::Data.define(version: 2026_04_22_155320) diff --git a/db/migrate/20260416075220_enable_post_gis.rb b/db/migrate/20260416075220_enable_post_gis.rb new file mode 100644 index 0000000000..54de1190ea --- /dev/null +++ b/db/migrate/20260416075220_enable_post_gis.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class EnablePostGIS < ActiveRecord::Migration[8.1] + def change + enable_extension "postgis" + end +end diff --git a/db/migrate/20260416132006_add_position_to_locations.rb b/db/migrate/20260416132006_add_position_to_locations.rb new file mode 100644 index 0000000000..a1595b2715 --- /dev/null +++ b/db/migrate/20260416132006_add_position_to_locations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPositionToLocations < ActiveRecord::Migration[8.1] + def change + add_column :locations, :position, :st_point, geographic: true + end +end diff --git a/db/migrate/20260417084418_change_notify_log_entry_purpose_to_not_null.rb b/db/migrate/20260417084418_change_notify_log_entry_purpose_to_not_null.rb new file mode 100644 index 0000000000..1866f204ec --- /dev/null +++ b/db/migrate/20260417084418_change_notify_log_entry_purpose_to_not_null.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ChangeNotifyLogEntryPurposeToNotNull < ActiveRecord::Migration[8.1] + def change + change_column_null :notify_log_entries, :purpose, false + end +end diff --git a/db/migrate/20260420122440_validate_patient_foreign_keys.rb b/db/migrate/20260420122440_validate_patient_foreign_keys.rb new file mode 100644 index 0000000000..06e8bc066d --- /dev/null +++ b/db/migrate/20260420122440_validate_patient_foreign_keys.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ValidatePatientForeignKeys < ActiveRecord::Migration[8.1] + TABLES_TO_CASCADE = %w[ + notify_log_entries + school_move_log_entries + patient_merge_log_entries + pds_search_results + patient_programme_vaccinations_searches + ].freeze + + def change + TABLES_TO_CASCADE.each { |table| validate_foreign_key table, "patients" } + end +end diff --git a/db/schema.rb b/db/schema.rb index 6c33e2c2ee..13e90601f7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,11 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_04_20_101139) do +ActiveRecord::Schema[8.1].define(version: 2026_04_20_122440) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_trgm" + enable_extension "postgis" # Custom types defined in this database. # Note that some types may not work with other database engines. Be careful if changing database. @@ -535,6 +536,7 @@ t.integer "gias_year_groups", default: [], null: false, array: true t.text "name", null: false t.string "ods_code" + t.geography "position", limit: {srid: 4326, type: "st_point", geographic: true} t.string "site" t.integer "status", default: 0, null: false t.string "systm_one_code" @@ -568,7 +570,7 @@ t.integer "delivery_status", default: 0, null: false t.bigint "parent_id" t.bigint "patient_id" - t.integer "purpose" + t.integer "purpose", null: false t.string "recipient", null: false t.bigint "sent_by_user_id" t.text "subject" @@ -1150,7 +1152,7 @@ add_foreign_key "notes", "users", column: "created_by_user_id" add_foreign_key "notify_log_entries", "consent_forms" add_foreign_key "notify_log_entries", "parents", on_delete: :nullify - add_foreign_key "notify_log_entries", "patients", on_delete: :cascade, validate: false + add_foreign_key "notify_log_entries", "patients", on_delete: :cascade add_foreign_key "notify_log_entries", "users", column: "sent_by_user_id" add_foreign_key "notify_log_entry_programmes", "notify_log_entries", on_delete: :cascade add_foreign_key "parent_relationships", "parents" @@ -1159,10 +1161,10 @@ add_foreign_key "patient_changesets", "patients" add_foreign_key "patient_locations", "locations" add_foreign_key "patient_locations", "patients" - add_foreign_key "patient_merge_log_entries", "patients", on_delete: :cascade, validate: false + add_foreign_key "patient_merge_log_entries", "patients", on_delete: :cascade add_foreign_key "patient_merge_log_entries", "users" add_foreign_key "patient_programme_statuses", "patients", on_delete: :cascade - add_foreign_key "patient_programme_vaccinations_searches", "patients", on_delete: :cascade, validate: false + add_foreign_key "patient_programme_vaccinations_searches", "patients", on_delete: :cascade add_foreign_key "patient_registration_statuses", "patients", on_delete: :cascade add_foreign_key "patient_registration_statuses", "sessions", on_delete: :cascade add_foreign_key "patient_specific_directions", "patients" @@ -1173,13 +1175,13 @@ add_foreign_key "patient_teams", "teams", on_delete: :cascade add_foreign_key "patients", "locations", column: "gp_practice_id" add_foreign_key "patients", "locations", column: "school_id" - add_foreign_key "pds_search_results", "patients", on_delete: :cascade, validate: false + add_foreign_key "pds_search_results", "patients", on_delete: :cascade add_foreign_key "pre_screenings", "locations" add_foreign_key "pre_screenings", "patients" add_foreign_key "pre_screenings", "users", column: "performed_by_user_id" add_foreign_key "reporting_api_one_time_tokens", "users" add_foreign_key "school_move_log_entries", "locations", column: "school_id" - add_foreign_key "school_move_log_entries", "patients", on_delete: :cascade, validate: false + add_foreign_key "school_move_log_entries", "patients", on_delete: :cascade add_foreign_key "school_move_log_entries", "teams" add_foreign_key "school_move_log_entries", "users" add_foreign_key "school_moves", "locations", column: "school_id" diff --git a/lib/tasks/smoke.rake b/lib/tasks/smoke.rake index d403da6a6a..0e4bcc2bfa 100644 --- a/lib/tasks/smoke.rake +++ b/lib/tasks/smoke.rake @@ -22,4 +22,10 @@ namespace :smoke do type: "gp_practice" ) end + + desc "Test the integration with the OS Places API by looking up a known location." + task os_places_api: :environment do + response = OrdnanceSurvey::PlacesAPI.find("The Shard, London") + puts "Found #{response[:results].count} results" + end end diff --git a/spec/components/app_activity_log_component_spec.rb b/spec/components/app_activity_log_component_spec.rb index a8c8285761..038ce34113 100644 --- a/spec/components/app_activity_log_component_spec.rb +++ b/spec/components/app_activity_log_component_spec.rb @@ -76,6 +76,8 @@ end describe "archive reasons" do + let(:component) { described_class.new(patient:, team:) } + before do create( :archive_reason, @@ -787,6 +789,8 @@ end describe "patient merge events" do + let(:component) { described_class.new(patient:, team:) } + before do create( :patient_merge_log_entry, diff --git a/spec/components/app_flash_message_component_spec.rb b/spec/components/app_flash_message_component_spec.rb index ec9306642d..584c8c7ff2 100644 --- a/spec/components/app_flash_message_component_spec.rb +++ b/spec/components/app_flash_message_component_spec.rb @@ -103,7 +103,7 @@ context "when body contains HTML that is marked as safe" do before { flash[:success][:body] = "

HTML

".html_safe } - it "doesn't render the body as HTML" do + it "renders the body as HTML" do expect( rendered.css(".nhsuk-notification-banner__content").inner_html ).to(include("

HTML

")) diff --git a/spec/controllers/api/reporting/totals_controller_spec.rb b/spec/controllers/api/reporting/totals_controller_spec.rb index 6db08908db..6e722e3e1d 100644 --- a/spec/controllers/api/reporting/totals_controller_spec.rb +++ b/spec/controllers/api/reporting/totals_controller_spec.rb @@ -19,6 +19,8 @@ expect(parsed_response).to have_key("not_vaccinated") expect(parsed_response).to have_key("vaccinations_given") expect(parsed_response).to have_key("monthly_vaccinations_given") + expect(parsed_response).to have_key("consent_refusal_reasons") + expect(parsed_response).to have_key("consent_routes") end it "calculates statistics correctly" do @@ -357,6 +359,253 @@ expect(parsed_response["consent_conflicts"]).to eq(1) expect(parsed_response["consent_no_response"]).to eq(4) end + + describe "consent breakdowns" do + let(:team) { Team.last } + let(:programme) { Programme.hpv } + let(:session) { create(:session, team:, programmes: [programme]) } + + before { team.programmes << programme } + + it "returns the breakdown shape with zero counts when no consents exist" do + create(:patient, session:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + expect(parsed_response["consent_refusal_reasons"]).to eq( + "contains_gelatine" => 0, + "already_vaccinated" => 0, + "will_be_vaccinated_elsewhere" => 0, + "medical_reasons" => 0, + "personal_choice" => 0, + "other" => 0, + "do_not_want_vaccination_at_school" => 0 + ) + expect(parsed_response["consent_routes"]).to eq( + "website" => 0, + "phone" => 0, + "paper" => 0, + "in_person" => 0, + "self_consent" => 0 + ) + end + + it "counts each parent's refusal reason when two parents refuse the same patient" do + patient = create(:patient, session:) + parent_one = create(:parent) + parent_two = create(:parent) + create(:parent_relationship, patient:, parent: parent_one) + create(:parent_relationship, patient:, parent: parent_two) + create( + :consent, + :refused, + patient:, + programme:, + team:, + parent: parent_one, + reason_for_refusal: "medical_reasons" + ) + create( + :consent, + :refused, + patient:, + programme:, + team:, + parent: parent_two, + reason_for_refusal: "personal_choice" + ) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + reasons = parsed_response["consent_refusal_reasons"] + expect(reasons["medical_reasons"]).to eq(1) + expect(reasons["personal_choice"]).to eq(1) + end + + it "counts each parent's route when two parents respond via different routes" do + patient = create(:patient, session:) + parent_one = create(:parent) + parent_two = create(:parent) + recorded_by = create(:user) + create(:parent_relationship, patient:, parent: parent_one) + create(:parent_relationship, patient:, parent: parent_two) + create( + :consent, + :given, + patient:, + programme:, + team:, + parent: parent_one, + route: "website" + ) + create( + :consent, + :given, + patient:, + programme:, + team:, + parent: parent_two, + route: "phone", + recorded_by: + ) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + routes = parsed_response["consent_routes"] + expect(routes["website"]).to eq(1) + expect(routes["phone"]).to eq(1) + end + + it "counts only the latest consent when one parent updates their refusal reason" do + patient = create(:patient, session:) + parent = create(:parent) + create(:parent_relationship, patient:, parent:) + create( + :consent, + :refused, + patient:, + programme:, + team:, + parent:, + reason_for_refusal: "medical_reasons", + submitted_at: 2.days.ago + ) + create( + :consent, + :refused, + patient:, + programme:, + team:, + parent:, + reason_for_refusal: "personal_choice", + submitted_at: 1.day.ago + ) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + reasons = parsed_response["consent_refusal_reasons"] + expect(reasons["medical_reasons"]).to eq(0) + expect(reasons["personal_choice"]).to eq(1) + end + + it "counts the refusal reason for a patient in conflicts status" do + patient = create(:patient, session:) + parent_giving = create(:parent) + parent_refusing = create(:parent) + create(:parent_relationship, patient:, parent: parent_giving) + create(:parent_relationship, patient:, parent: parent_refusing) + create( + :consent, + :given, + patient:, + programme:, + team:, + parent: parent_giving, + route: "website" + ) + create( + :consent, + :refused, + patient:, + programme:, + team:, + parent: parent_refusing, + reason_for_refusal: "medical_reasons", + route: "phone", + recorded_by: create(:user) + ) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + expect(parsed_response["consent_conflicts"]).to eq(1) + expect( + parsed_response["consent_refusal_reasons"]["medical_reasons"] + ).to eq(1) + expect(parsed_response["consent_routes"]["website"]).to eq(1) + expect(parsed_response["consent_routes"]["phone"]).to eq(1) + end + + it "counts both self-consent and parental consent on the same patient" do + patient = create(:patient, session:, year_group: 11) + parent = create(:parent) + create(:parent_relationship, patient:, parent:) + create( + :consent, + :given, + patient:, + programme:, + team:, + parent:, + route: "website" + ) + create(:consent, :given, :self_consent, patient:, programme:, team:) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + routes = parsed_response["consent_routes"] + expect(routes["website"]).to eq(1) + expect(routes["self_consent"]).to eq(1) + end + + it "excludes invalidated consents but includes withdrawn consents" do + patient = create(:patient, session:) + parent_one = create(:parent) + parent_two = create(:parent) + create(:parent_relationship, patient:, parent: parent_one) + create(:parent_relationship, patient:, parent: parent_two) + create( + :consent, + :refused, + :invalidated, + patient:, + programme:, + team:, + parent: parent_one, + reason_for_refusal: "medical_reasons", + route: "website" + ) + create( + :consent, + :withdrawn, + patient:, + programme:, + team:, + parent: parent_two, + reason_for_refusal: "personal_choice", + route: "self_consent" + ) + PatientStatusUpdater.call(patient:) + + refresh_reporting_views! + + get :index, params: { programme: "hpv" } + + reasons = parsed_response["consent_refusal_reasons"] + expect(reasons["medical_reasons"]).to eq(0) + expect(reasons["personal_choice"]).to eq(1) + + routes = parsed_response["consent_routes"] + expect(routes["website"]).to eq(0) + expect(routes["self_consent"]).to eq(1) + end + end end describe "#index.csv" do diff --git a/spec/factories/locations.rb b/spec/factories/locations.rb index 6eba909ae0..db14b8cd0c 100644 --- a/spec/factories/locations.rb +++ b/spec/factories/locations.rb @@ -15,6 +15,7 @@ # gias_year_groups :integer default([]), not null, is an Array # name :text not null # ods_code :string +# position :geography point, 4326 # site :string # status :integer default("unknown"), not null # systm_one_code :string @@ -52,6 +53,9 @@ address_line_1 { Faker::Address.street_address } address_town { Faker::Address.city } address_postcode { Faker::Address.uk_postcode } + position do + "POINT(#{Faker::Address.longitude} #{Faker::Address.latitude})" + end end factory :community_clinic do diff --git a/spec/factories/notify_log_entries.rb b/spec/factories/notify_log_entries.rb index 94f6dc5ed6..75131f6394 100644 --- a/spec/factories/notify_log_entries.rb +++ b/spec/factories/notify_log_entries.rb @@ -7,7 +7,7 @@ # id :bigint not null, primary key # body :text # delivery_status :integer default("sending"), not null -# purpose :integer +# purpose :integer not null # recipient :string not null # subject :text # type :integer not null @@ -56,6 +56,9 @@ end delivery_id { SecureRandom.uuid } + purpose do + NotifyTemplate.find_by_id(template_id, channel: type.to_sym)&.purpose + end traits_for_enum :delivery_status traits_for_enum :purpose diff --git a/spec/features/archive_children_spec.rb b/spec/features/archive_children_spec.rb index 2f4a462692..eca8fcc9a1 100644 --- a/spec/features/archive_children_spec.rb +++ b/spec/features/archive_children_spec.rb @@ -224,7 +224,6 @@ def and_i_see_an_archived_tag end def and_i_see_an_activity_log_entry - within(".app-secondary-navigation") { click_on "HPV" } expect(page).to have_content("Record archived:") end diff --git a/spec/features/manage_children_spec.rb b/spec/features/manage_children_spec.rb index 58c1df7a0a..7cb2ec59fe 100644 --- a/spec/features/manage_children_spec.rb +++ b/spec/features/manage_children_spec.rb @@ -75,7 +75,6 @@ and_the_vaccination_record_is_updated_with_the_nhs when_i_go_back_to_the_patient_page - and_i_click_on_a_programme then_i_see_the_patient_merge_in_the_activity_log end diff --git a/spec/features/national_reporting_team_spec.rb b/spec/features/national_reporting_team_spec.rb index b00adf178a..91fa79b900 100644 --- a/spec/features/national_reporting_team_spec.rb +++ b/spec/features/national_reporting_team_spec.rb @@ -182,7 +182,7 @@ def then_i_should_see_the_childs_card def then_i_should_see_vaccinations_then_child_details app_cards = page.all(".app-card") - expect(app_cards.count).to eq(2) + expect(app_cards.count).to eq(3) expect(app_cards[0]).to have_content("Vaccinations") expect(app_cards[1]).to have_content("Child record") end diff --git a/spec/features/verbal_consent_given_keep_in_triage_spec.rb b/spec/features/verbal_consent_given_keep_in_triage_spec.rb index 0d5b8615ee..6a7ba71ee1 100644 --- a/spec/features/verbal_consent_given_keep_in_triage_spec.rb +++ b/spec/features/verbal_consent_given_keep_in_triage_spec.rb @@ -74,8 +74,8 @@ def then_an_email_is_sent_to_the_parent_about_triage to: @parent.email, template: :consent_confirmation_triage ).with_content_including( - "we need to review your answers", - "We’ll let you know once we’ve done this" + "we’ll review your answers", + "We’ll contact you if we need to delay or cancel the vaccination." ) ) end diff --git a/spec/jobs/enqueue_automated_careplus_reports_job_spec.rb b/spec/jobs/enqueue_automated_careplus_reports_job_spec.rb new file mode 100644 index 0000000000..dcf18b16d8 --- /dev/null +++ b/spec/jobs/enqueue_automated_careplus_reports_job_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe EnqueueAutomatedCareplusReportsJob do + subject(:perform) { described_class.new.perform } + + let(:eligible_team) do + create(:team, :with_careplus_enabled, programmes: Programme.all) + end + let(:team_without_credentials) do + create( + :team, + :with_careplus_enabled, + careplus_username: nil, + programmes: Programme.all + ) + end + let(:team_without_careplus_report_fields) do + create( + :team, + careplus_username: "careplus_user", + careplus_password: "careplus_password", + careplus_namespace: "MOCK", + programmes: Programme.all + ) + end + + it "enqueues a send job for each team with CarePlus enabled and credentials configured" do + eligible_team + team_without_credentials + team_without_careplus_report_fields + + expect { perform }.to enqueue_sidekiq_job( + SendAutomatedCareplusReportsJob + ).once.with(eligible_team.id) + end +end diff --git a/spec/jobs/send_automated_careplus_reports_job_spec.rb b/spec/jobs/send_automated_careplus_reports_job_spec.rb new file mode 100644 index 0000000000..a79aedc5bc --- /dev/null +++ b/spec/jobs/send_automated_careplus_reports_job_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +describe SendAutomatedCareplusReportsJob do + describe "#perform" do + it "delegates to Careplus::AutomatedReportSender" do + team = create(:team, :with_careplus_enabled, programmes: Programme.all) + + expect(Careplus::AutomatedReportSender).to receive(:call).with( + team_id: team.id + ) + + described_class.new.perform(team.id) + end + end +end diff --git a/spec/lib/careplus/automated_report_sender_spec.rb b/spec/lib/careplus/automated_report_sender_spec.rb new file mode 100644 index 0000000000..771a014671 --- /dev/null +++ b/spec/lib/careplus/automated_report_sender_spec.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +describe Careplus::AutomatedReportSender do + subject(:call) { described_class.call(team_id: team.id) } + + let(:team) do + create(:team, :with_careplus_enabled, programmes: Programme.all) + end + let(:programme) { Programme.hpv } + let(:session) { create(:session, team:, programmes: [programme]) } + let(:endpoint) do + "#{Settings.careplus.base_url}/#{team.careplus_namespace}/soap.SchImms.cls" + end + let(:response_status) { 200 } + let(:response_body) { "OK" } + let(:yesterday) { Date.new(2025, 8, 31) } + let(:yesterday_academic_year) { yesterday.academic_year } + + before do + stub_request(:post, endpoint).to_return( + status: response_status, + body: response_body + ) + end + + around { |example| travel_to(Date.new(2025, 9, 1)) { example.run } } + + it "creates, sends, and stores a sent report with linked vaccination records" do + record = + create( + :vaccination_record, + session:, + programme:, + performed_at: yesterday, + created_at: yesterday, + updated_at: yesterday + ) + + expect { call }.to change(CareplusReport, :count).by(1).and( + change(CareplusReportVaccinationRecord, :count).by(1) + ) + + report = CareplusReport.last + + expect(report).to have_attributes( + team:, + academic_year: yesterday_academic_year, + date_from: yesterday, + date_to: yesterday, + status: "sent" + ) + expect(report.sent_at).to be_present + expect(report.vaccination_records).to contain_exactly(record) + expect(WebMock).to have_requested(:post, endpoint).once + end + + it "uses yesterday and its academic year for the automated export scope" do + create( + :vaccination_record, + session:, + programme:, + performed_at: yesterday, + created_at: yesterday, + updated_at: yesterday + ) + + expect(Reports::AutomatedCareplusExporter).to receive( + :vaccination_records_scope + ).with( + team:, + academic_year: yesterday_academic_year, + start_date: yesterday, + end_date: yesterday + ).and_call_original + + call + end + + it "splits yesterday's scope into batches of 10000 records" do + stub_const("#{described_class}::BATCH_SIZE", 2) + records = + Array.new(3) do + create( + :vaccination_record, + session:, + programme:, + performed_at: yesterday, + created_at: yesterday, + updated_at: yesterday + ) + end + + expect { call }.to change(CareplusReport, :count).by(2).and( + change(CareplusReportVaccinationRecord, :count).by(3) + ) + + expect(WebMock).to have_requested(:post, endpoint).twice + expect( + CareplusReport + .order(:id) + .map { |report| report.vaccination_records.count } + ).to eq([2, 1]) + expect( + CareplusReport + .joins(:vaccination_records) + .distinct + .flat_map(&:vaccination_records) + ).to match_array(records) + end + + context "when CarePlus returns a failure response" do + let(:response_status) { 500 } + let(:response_body) { "Error" } + + it "marks the report as failed, keeps linked vaccination records, and raises for retry" do + record = + create( + :vaccination_record, + session:, + programme:, + performed_at: yesterday, + created_at: yesterday, + updated_at: yesterday + ) + + expect { call }.to raise_error( + described_class::FailedResponseError, + "CarePlus request failed with HTTP 500: " + ).and change(CareplusReport, :count).by(1).and( + change(CareplusReportVaccinationRecord, :count).by(1) + ) + + report = CareplusReport.last + expect(report).to have_attributes(status: "failed") + expect(report.sent_at).to be_present + expect(report.vaccination_records).to contain_exactly(record) + end + end + + context "when CarePlus raises an error" do + before { stub_request(:post, endpoint).to_raise(Timeout::Error) } + + it "marks the report as failed and keeps linked vaccination records before re-raising" do + record = + create( + :vaccination_record, + session:, + programme:, + performed_at: yesterday, + created_at: yesterday, + updated_at: yesterday + ) + + expect { call }.to raise_error(Timeout::Error) + + report = CareplusReport.last + expect(report).to have_attributes(status: "failed") + expect(report.sent_at).to be_present + expect(report.vaccination_records).to contain_exactly(record) + end + end + + context "when the team is no longer eligible for automated reports" do + before { team.update!(careplus_username: nil) } + + it "does nothing" do + expect { call }.not_to change(CareplusReport, :count) + end + end +end diff --git a/spec/lib/careplus/client_spec.rb b/spec/lib/careplus/client_spec.rb index 632b6b80bb..d62cbac614 100644 --- a/spec/lib/careplus/client_spec.rb +++ b/spec/lib/careplus/client_spec.rb @@ -74,6 +74,14 @@ end end + context "when base_url is not configured" do + before { allow(Settings.careplus).to receive(:base_url).and_return(nil) } + + it "raises a RuntimeError" do + expect { response }.to raise_error(RuntimeError) + end + end + context "when base_url uses HTTPS" do before do allow(Settings.careplus).to receive(:base_url).and_return( diff --git a/spec/lib/notify_template_spec.rb b/spec/lib/notify_template_spec.rb index 6964a78214..5049990c5a 100644 --- a/spec/lib/notify_template_spec.rb +++ b/spec/lib/notify_template_spec.rb @@ -99,6 +99,41 @@ end end + describe "#purpose" do + it "returns the symbol declared in frontmatter" do + content = "---\ntemplate_id: \"abc\"\npurpose: consent_request\n---\nbody" + template = described_class.new(name: :test, channel: :email, content:) + expect(template.purpose).to eq(:consent_request) + end + end + + describe "purpose validation" do + def build(extra_frontmatter = "") + content = "---\ntemplate_id: \"abc\"\n#{extra_frontmatter}---\nbody" + described_class.new(name: :test, channel: :email, content:) + end + + it "is valid with a known purpose" do + expect(build("purpose: consent_request\n")).to be_valid + end + + it "is invalid when purpose is missing" do + template = build + expect(template).to be_invalid + expect(template.errors[:purpose]).to include( + "Add a purpose to the template frontmatter" + ) + end + + it "is invalid when purpose is unknown" do + template = build("purpose: nonsense_value\n") + expect(template).to be_invalid + expect(template.errors[:purpose]).to include( + "Use a purpose from the list of known purposes in NotifyLogEntry" + ) + end + end + describe "frontmatter parsing" do it "parses frontmatter and body from ERB content" do content = "---\ntemplate_id: \"abc\"\n---\nHello world\n" diff --git a/spec/lib/ordnance_survey/places_api_spec.rb b/spec/lib/ordnance_survey/places_api_spec.rb new file mode 100644 index 0000000000..3f38823c75 --- /dev/null +++ b/spec/lib/ordnance_survey/places_api_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +describe OrdnanceSurvey::PlacesAPI do + let(:api_key) { "test_api_key" } + + before { Settings.ordnance_survey.api_key = api_key } + + after { Settings.reload! } + + describe "#find" do + subject(:find) { described_class.find(query, **options) } + + let(:query) { "1 High Street, London" } + let(:options) { {} } + let(:api_url) { "https://api.os.uk" } + + context "when the request is successful" do + let(:response_body) do + { + header: { + uri: + "#{api_url}/search/places/v1/find?query=1+High+Street%2C+London&format=json", + query: "1 High Street, London", + offset: 0, + total_results: 1, + format: "json", + dataset: "DPA", + lr: "EN,GB", + max_results: 100, + epoch: "111", + output_srs: "EPSG:27700" + }, + results: [ + { + DPA: { + UPRN: "1000000000", + UDPRN: "12345", + ADDRESSBASE: "GB12345678", + ADDRESSBASE_POSTCODE: "SW1A 1AA", + BUILDING_NAME: "Test Building", + BUILDING_NUMBER: "1", + SUB_BUILDING_NAME: "Flat 1", + THOROUGHFARE_NAME: "High Street", + THOROUGHFARE_DESCRIPTOR: "", + POSTTOWN: "London", + POSTCODE: "SW1A 1AA", + POSTCODE_TYPE: "L", + LATITUDE: 51.5074, + LONGITUDE: -0.1278, + X_COORDINATE: 530_000.0, + Y_COORDINATE: 180_000.0, + EASTING: 530_000, + NORTHING: 180_000, + COUNTRY: "England" + }, + ADDRESS: "1, High Street, London, SW1A 1AA", + BUILDING_NUMBER: "1", + THOROUGHFARE: "High Street", + LOCALITY: "", + TOWN: "London", + POSTCODE: "SW1A 1AA", + COUNTY: "Greater London", + COUNTRY: "England", + UPRN: "1000000000", + MATCH: 1.0, + MATCH_DESCRIPTION: "EXACT", + DISTANCE: 0.0 + } + ] + }.to_json + end + + before do + stub_request(:get, "#{api_url}/search/places/v1/find").with( + query: hash_including(query:, format: "json"), + headers: { + "Key" => api_key + } + ).to_return( + status: 200, + body: response_body, + headers: { + "Content-Type" => "application/json" + } + ) + end + + it "returns parsed results" do + response = find + + expect(response[:results].length).to eq(1) + expect(response[:results][0][:postcode]).to eq("SW1A 1AA") + expect(response[:results][0][:building_number]).to eq("1") + expect(response[:results][0][:thoroughfare]).to eq("High Street") + expect(response[:results][0][:town]).to eq("London") + expect(response[:results][0][:country]).to eq("England") + expect(response[:results][0][:uprn]).to eq("1000000000") + expect(response[:results][0][:match]).to eq(1.0) + expect(response[:results][0][:match_description]).to eq("EXACT") + end + end + + context "when the request is invalid" do + before do + stub_request(:get, "#{api_url}/search/places/v1/find").with( + query: hash_including(query:, format: "json"), + headers: { + "Key" => api_key + } + ).to_return( + status: 400, + body: { error: "Invalid query" }.to_json, + headers: { + } + ) + end + + it "raises an error" do + expect { find }.to raise_error(Faraday::BadRequestError) + end + end + + context "when rate limit is exceeded" do + before do + stub_request(:get, "#{api_url}/search/places/v1/find").with( + query: hash_including(query:, format: "json"), + headers: { + "Key" => api_key + } + ).to_return(status: 429, body: "Rate limit exceeded", headers: {}) + end + + it "raises an error" do + expect { find }.to raise_error(Faraday::ClientError) + end + end + + context "when there is an unexpected error" do + before do + stub_request(:get, "#{api_url}/search/places/v1/find").with( + query: hash_including(query:, format: "json"), + headers: { + "Key" => api_key + } + ).to_return(status: 500, body: "Internal Server Error", headers: {}) + end + + it "raises an error" do + expect { find }.to raise_error(Faraday::ServerError) + end + end + end +end diff --git a/spec/models/immunisation_import_row_spec.rb b/spec/models/immunisation_import_row_spec.rb index 0b7805beab..619199dfa3 100644 --- a/spec/models/immunisation_import_row_spec.rb +++ b/spec/models/immunisation_import_row_spec.rb @@ -575,6 +575,18 @@ end end + context "with an invalid dose sequence for MMR" do + let(:programmes) { [Programme.mmr] } + let(:data) { { "PROGRAMME" => "MMR", "DOSE_SEQUENCE" => "unknown" } } + + it "has the correct error message" do + immunisation_import_row.valid? + expect(immunisation_import_row.errors["DOSE_SEQUENCE"]).to include( + "Enter a dose sequence number, for example, 1 or 2." + ) + end + end + context "with an invalid dose sequence" do let(:programmes) { [Programme.hpv] } @@ -2148,11 +2160,27 @@ context "matching logic" do context "with a brand new patient" do + let(:local_authority) do + create(:local_authority, mhclg_code: "abc", gss_code: "123") + end + + before do + create( + :local_authority_postcode, + gss_code: local_authority.gss_code, + value: address_postcode + ) + end + its(:given_name) { should eq given_name } its(:family_name) { should eq family_name } its(:date_of_birth) { should eq Date.parse(date_of_birth) } its(:address_postcode) { should eq address_postcode } + its(:local_authority_mhclg_code) do + should eq local_authority.mhclg_code + end + it "doesn't add a patient to the database" do expect { patient }.not_to change(Patient, :count) end diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 02cf9fdf10..6ac193a507 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -15,6 +15,7 @@ # gias_year_groups :integer default([]), not null, is an Array # name :text not null # ods_code :string +# position :geography point, 4326 # site :string # status :integer default("unknown"), not null # systm_one_code :string @@ -432,6 +433,7 @@ "is_attached_to_team" => false, "name" => location.name, "ods_code" => location.ods_code, + "position" => [location.position.x, location.position.y], "site" => location.site, "status" => "unknown", "type" => "community_clinic", diff --git a/spec/models/notify_log_entry_spec.rb b/spec/models/notify_log_entry_spec.rb index ce4302212e..5b3a0d75ef 100644 --- a/spec/models/notify_log_entry_spec.rb +++ b/spec/models/notify_log_entry_spec.rb @@ -7,7 +7,7 @@ # id :bigint not null, primary key # body :text # delivery_status :integer default("sending"), not null -# purpose :integer +# purpose :integer not null # recipient :string not null # subject :text # type :integer not null @@ -41,6 +41,8 @@ let(:type) { :email } it { should be_valid } + it { should validate_presence_of(:purpose) } + it { should allow_value(:consent_request).for(:purpose) } end context "with an SMS type" do @@ -49,102 +51,6 @@ it { should be_valid } end - describe ".purpose_for_template_name" do - subject(:purpose) do - described_class.purpose_for_template_name(template_name) - end - - context "when the template indicates a consent request" do - let(:template_name) { :consent_school_request } - - it { should eq(:consent_request) } - end - - context "when the template indicates a consent reminder" do - let(:template_name) { :consent_school_reminder } - - it { should eq(:consent_reminder) } - end - - context "when the template indicates a consent confirmation" do - let(:template_name) { :consent_confirmation_given } - - it { should eq(:consent_confirmation) } - end - - context "when the template indicates a consent warning" do - let(:template_name) { :consent_unknown_contact_details_warning } - - it { should eq(:consent_warning) } - end - - context "when the template indicates a clinic invitation" do - let(:template_name) { :clinic_flu_invitation } - - it { should eq(:clinic_invitation) } - end - - context "when the template indicates a session reminder" do - let(:template_name) { :session_school_reminder_today } - - it { should eq(:session_reminder) } - end - - context "when the template indicates triage vaccination will happen" do - let(:template_name) { :triage_vaccination_will_happen_outcome } - - it { should eq(:triage_vaccination_will_happen) } - end - - context "when the template indicates triage vaccination won't happen" do - let(:template_name) { :triage_vaccination_wont_happen_outcome } - - it { should eq(:triage_vaccination_wont_happen) } - end - - context "when the template indicates triage vaccination at clinic" do - let(:template_name) { :triage_vaccination_at_clinic_outcome } - - it { should eq(:triage_vaccination_at_clinic) } - end - - context "when the template indicates a triage delay vaccination update" do - let(:template_name) { :triage_delay_vaccination_outcome } - - it { should eq(:triage_delay_vaccination) } - end - - context "when the template indicates vaccination administered" do - let(:template_name) { :vaccination_administered_notification } - - it { should eq(:vaccination_administered) } - end - - context "when the template indicates vaccination already had" do - let(:template_name) { :vaccination_already_had_notification } - - it { should eq(:vaccination_already_had) } - end - - context "when the template indicates vaccination not administered" do - let(:template_name) { :vaccination_not_administered_notification } - - it { should eq(:vaccination_not_administered) } - end - - context "when the template indicates vaccination deleted" do - let(:template_name) { :vaccination_deleted_notification } - - it { should eq(:vaccination_deleted) } - end - - context "when the template name does not match any known purpose" do - let(:template_name) { :something_else_entirely } - - it { should be_nil } - end - end - describe "#title" do subject(:title) { notify_log_entry.title } diff --git a/spec/requests/api/testing/vaccinations_search_in_nhs_spec.rb b/spec/requests/api/testing/vaccinations_search_in_nhs_spec.rb new file mode 100644 index 0000000000..bde1d7a3e4 --- /dev/null +++ b/spec/requests/api/testing/vaccinations_search_in_nhs_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe "/api/testing/vaccinations-search-in-nhs" do + before { Flipper.enable(:testing_api) } + after { Flipper.disable(:testing_api) } + + describe "POST" do + context "without wait param" do + it "enqueues the job and responds with accepted" do + expect { + post "/api/testing/vaccinations-search-in-nhs" + }.to have_enqueued_job(EnqueueVaccinationsSearchInNHSJob) + expect(response).to have_http_status(:accepted) + end + end + + context "with wait=true" do + before do + allow(EnqueueVaccinationsSearchInNHSJob).to receive(:perform_now) + end + + it "runs the job synchronously and responds with ok" do + post "/api/testing/vaccinations-search-in-nhs", params: { wait: "true" } + expect(EnqueueVaccinationsSearchInNHSJob).to have_received(:perform_now) + expect(response).to have_http_status(:ok) + end + end + end +end