diff --git a/Gemfile.lock b/Gemfile.lock index 6bba51e67d..ad9d30dcc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,11 +110,11 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.3.2) - aws-partitions (1.1090.0) + aws-partitions (1.1103.0) aws-sdk-accessanalyzer (1.70.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-core (3.223.0) + aws-sdk-core (3.224.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -130,13 +130,13 @@ GEM aws-sdk-iam (1.120.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-kms (1.100.0) + aws-sdk-kms (1.101.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) aws-sdk-rds (1.269.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.185.0) + aws-sdk-s3 (1.186.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -187,7 +187,7 @@ GEM crass (1.0.6) cssbundling-rails (1.4.1) railties (>= 6.0.0) - csv (3.3.2) + csv (3.3.4) cuprite (0.15.1) capybara (~> 3.0) ferrum (~> 0.15.0) @@ -202,7 +202,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.6.1) + diff-lcs (1.6.2) discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.0) @@ -329,7 +329,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -416,7 +416,7 @@ GEM ast (~> 2.4.1) racc pg (1.5.9) - phonelib (0.10.4) + phonelib (0.10.8) pp (0.6.2) prettyprint prettier_print (1.2.1) @@ -432,7 +432,7 @@ GEM method_source (~> 1.0) pry-rails (0.3.11) pry (>= 0.13.0) - psych (5.2.3) + psych (5.2.6) date stringio public_suffix (6.0.1) @@ -520,24 +520,24 @@ GEM rspec-mocks (~> 3.13.0) rspec-core (3.13.3) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.2) + rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.1.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) + rspec-rails (8.0.0) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.2) + rspec-support (3.13.3) rubocop (1.75.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -595,7 +595,7 @@ GEM sentry-ruby (5.23.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - shoulda-matchers (6.4.0) + shoulda-matchers (6.5.0) activesupport (>= 5.2.0) simplecov (0.22.0) docile (~> 1.1) diff --git a/app/assets/stylesheets/_grid.scss b/app/assets/stylesheets/_grid.scss index fd4d682058..ff782c0e3e 100644 --- a/app/assets/stylesheets/_grid.scss +++ b/app/assets/stylesheets/_grid.scss @@ -1,5 +1,5 @@ // Search filters and results -.app-grid-column-filters > .nhsuk-card--feature, +.app-grid-column-filters .nhsuk-card--feature.app-filters, .app-grid-column-results > .nhsuk-card--feature, .app-grid-column-results > .nhsuk-warning-callout { margin-top: nhsuk-spacing(3); diff --git a/app/components/app_programme_stats_component.html.erb b/app/components/app_programme_stats_component.html.erb new file mode 100644 index 0000000000..fcbd9da71d --- /dev/null +++ b/app/components/app_programme_stats_component.html.erb @@ -0,0 +1,22 @@ +
+
+ <%= render AppCardComponent.new(link_to: programme_cohorts_path(@programme), colour: "reversed", data: true) do |card| %> + <% card.with_heading { "Children" } %> + <% card.with_description { patients_count.to_s } %> + <% end %> +
+ +
+ <%= render AppCardComponent.new(link_to: programme_vaccination_records_path(@programme), colour: "reversed", data: true) do |card| %> + <% card.with_heading { "Vaccinations" } %> + <% card.with_description { vaccinations_count.to_s } %> + <% end %> +
+ +
+ <%= render AppCardComponent.new(data: true, colour: "reversed") do |card| %> + <% card.with_heading { "Consent requests and reminders sent" } %> + <% card.with_description { consent_notifications_count.to_s } %> + <% end %> +
+
\ No newline at end of file diff --git a/app/components/app_programme_stats_component.rb b/app/components/app_programme_stats_component.rb new file mode 100644 index 0000000000..521369c0e4 --- /dev/null +++ b/app/components/app_programme_stats_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AppProgrammeStatsComponent < ViewComponent::Base + def initialize(programme:) + super + @programme = programme + end + + def patients_count + helpers.policy_scope(Patient).in_programmes([@programme]).count + end + + def vaccinations_count + helpers.policy_scope(VaccinationRecord).where(programme: @programme).count + end + + def consent_notifications_count + helpers.policy_scope(ConsentNotification).has_programme(@programme).count + end +end diff --git a/app/controllers/cohorts_controller.rb b/app/controllers/cohorts_controller.rb index 8720028094..73c28372e2 100644 --- a/app/controllers/cohorts_controller.rb +++ b/app/controllers/cohorts_controller.rb @@ -15,6 +15,9 @@ def index .where(birth_academic_year: birth_academic_years) .group(:birth_academic_year) .count + .sort + .reverse + .to_h birth_academic_years.each do |birth_academic_year| @patient_count_by_birth_academic_year[birth_academic_year] ||= 0 diff --git a/app/controllers/concerns/authentication_concern.rb b/app/controllers/concerns/authentication_concern.rb index 402e8529ee..13b35f1e57 100644 --- a/app/controllers/concerns/authentication_concern.rb +++ b/app/controllers/concerns/authentication_concern.rb @@ -19,10 +19,12 @@ def authenticate_user! redirect_to start_path end elsif cis2_session? - if !selected_cis2_org_is_registered? - redirect_to users_organisation_not_found_path + if !selected_cis2_workgroup_is_valid? + redirect_to users_workgroup_not_found_path elsif !selected_cis2_role_is_valid? redirect_to users_role_not_found_path + elsif !selected_cis2_org_is_registered? + redirect_to users_organisation_not_found_path end end end @@ -38,8 +40,8 @@ def selected_cis2_org_is_registered? end def selected_cis2_workgroup_is_valid? - selected_cis2_nrbac_role.key?("workgroups") && - CIS2_WORKGROUP.in?(selected_cis2_nrbac_role["workgroups"]) + workgroups = session.dig("cis2_info", "selected_role", "workgroups") + workgroups.present? && CIS2_WORKGROUP.in?(workgroups) end def valid_cis2_roles diff --git a/app/controllers/parent_interface/consent_forms/base_controller.rb b/app/controllers/parent_interface/consent_forms/base_controller.rb index 1447902efd..d021f2d44e 100644 --- a/app/controllers/parent_interface/consent_forms/base_controller.rb +++ b/app/controllers/parent_interface/consent_forms/base_controller.rb @@ -5,6 +5,10 @@ class ConsentForms::BaseController < ApplicationController skip_before_action :authenticate_user! skip_after_action :verify_policy_scoped + prepend_before_action :set_team + prepend_before_action :set_programmes + prepend_before_action :set_organisation + prepend_before_action :set_session prepend_before_action :set_consent_form before_action :authenticate_consent_form_user! before_action :set_privacy_policy_url @@ -16,10 +20,41 @@ def set_consent_form ConsentForm.includes(:programmes, :vaccines).find( params[:consent_form_id] || params[:id] ) - @organisation = @consent_form.organisation - @programmes = @consent_form.programmes - @session = @consent_form.original_session - @team = @consent_form.team + end + + def set_session + if params[:session_slug] + @session = Session.find_by!(slug: params[:session_slug]) + elsif @consent_form.present? + @session = @consent_form.original_session + end + end + + def set_organisation + @organisation = + if @consent_form.present? + @consent_form.organisation + elsif @session.present? + @session.organisation + end + end + + def set_programmes + @programmes = + if @consent_form.present? + @consent_form.programmes + elsif @session.present? && params[:programme_types].present? + @session.programmes.where(type: params[:programme_types].split("-")) + end + end + + def set_team + @team = + if @consent_form.present? + @consent_form.team + elsif @session.present? + @session.team + end end def authenticate_consent_form_user! diff --git a/app/controllers/parent_interface/consent_forms_controller.rb b/app/controllers/parent_interface/consent_forms_controller.rb index ab58db0ab4..86150f8e2c 100644 --- a/app/controllers/parent_interface/consent_forms_controller.rb +++ b/app/controllers/parent_interface/consent_forms_controller.rb @@ -4,8 +4,6 @@ module ParentInterface class ConsentFormsController < ConsentForms::BaseController include ConsentFormMailerConcern - prepend_before_action :set_session_and_programmes, - only: %i[start create deadline_passed] skip_before_action :set_consent_form, only: %i[start create deadline_passed] skip_before_action :authenticate_consent_form_user!, only: %i[start create deadline_passed] @@ -63,21 +61,15 @@ def record private - def set_session_and_programmes - @session = Session.find_by!(slug: params[:session_slug]) - @organisation = @session.organisation - @programmes = - @session.programmes.where(type: params[:programme_types].split("-")) - @team = @session.team - end - def clear_session_edit_variables session.delete(:follow_up_changes_start_page) end def check_if_past_deadline return if @session.open_for_consent? - redirect_to action: :deadline_passed + redirect_to action: :deadline_passed, + programme_types: @programmes.map(&:type).join("-"), + session_slug: @session.slug end end end diff --git a/app/controllers/patients_controller.rb b/app/controllers/patients_controller.rb index 1981e98ab6..c2b2d451dd 100644 --- a/app/controllers/patients_controller.rb +++ b/app/controllers/patients_controller.rb @@ -9,10 +9,7 @@ class PatientsController < ApplicationController before_action :record_access_log_entry, only: %i[show log] def index - patients = - @form.apply( - policy_scope(Patient).includes(:school).not_deceased.order_by_name - ) + patients = @form.apply(policy_scope(Patient).includes(:school).not_deceased) @pagy, @patients = pagy(patients) diff --git a/app/controllers/programmes_controller.rb b/app/controllers/programmes_controller.rb index c0f3e0cc72..b2dd989832 100644 --- a/app/controllers/programmes_controller.rb +++ b/app/controllers/programmes_controller.rb @@ -17,12 +17,6 @@ def index def show patients = policy_scope(Patient).in_programmes([@programme]) - - @patients_count = patients.count - @vaccinations_count = - policy_scope(VaccinationRecord).where(programme: @programme).count - @consent_notifications_count = - @programme.consent_notifications.has_programme(@programme).count @consents = policy_scope(Consent).where(patient: patients, programme: @programme) end diff --git a/app/controllers/sessions/triage_controller.rb b/app/controllers/sessions/triage_controller.rb index cd27967a51..ac92688ba1 100644 --- a/app/controllers/sessions/triage_controller.rb +++ b/app/controllers/sessions/triage_controller.rb @@ -10,7 +10,7 @@ class Sessions::TriageController < ApplicationController layout "full" def show - @statuses = Patient::TriageStatus.statuses.keys - [:not_required] + @statuses = Patient::TriageStatus.statuses.keys - %w[not_required] @programmes = @session.programmes scope = diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index fea6a0fe96..c824112a3c 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -14,12 +14,12 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def cis2 set_cis2_session_info - if !selected_cis2_role_is_valid? + if !selected_cis2_workgroup_is_valid? + redirect_to users_workgroup_not_found_path + elsif !selected_cis2_role_is_valid? redirect_to users_role_not_found_path elsif !selected_cis2_org_is_registered? redirect_to users_organisation_not_found_path - elsif !selected_cis2_workgroup_is_valid? - redirect_to users_workgroup_not_found_path else @user = User.find_or_create_from_cis2_oidc(user_cis2_info) diff --git a/app/forms/batch_form.rb b/app/forms/batch_form.rb index b3dd441601..61c4326363 100644 --- a/app/forms/batch_form.rb +++ b/app/forms/batch_form.rb @@ -9,7 +9,15 @@ class BatchForm attribute :name, :string attribute :expiry, :date - validates :name, presence: true, format: { with: /\A[A-Za-z0-9]+\z/ } + validates :name, + presence: true, + format: { + with: /\A[A-Za-z0-9]+\z/ + }, + length: { + minimum: 2, + maximum: 100 + } validates :expiry, comparison: { diff --git a/app/helpers/phone_helper.rb b/app/helpers/phone_helper.rb new file mode 100644 index 0000000000..401be94abc --- /dev/null +++ b/app/helpers/phone_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module PhoneHelper + def format_phone_with_instructions(entity) + return entity.phone if entity.phone_instructions.blank? + + "#{entity.phone} (#{entity.phone_instructions})" + end +end diff --git a/app/jobs/patient_nhs_number_lookup_with_pending_changes_job.rb b/app/jobs/patient_nhs_number_lookup_with_pending_changes_job.rb new file mode 100644 index 0000000000..5b545c3353 --- /dev/null +++ b/app/jobs/patient_nhs_number_lookup_with_pending_changes_job.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class PatientNHSNumberLookupWithPendingChangesJob < ApplicationJob + include NHSAPIConcurrencyConcern + + queue_as :imports + + def perform(patient) + patient_with_pending_changes = patient.with_pending_changes + patient_with_pending_changes.nhs_number = + nil unless patient.nhs_number_changed? + + return unless patient_with_pending_changes.changed? + + if patient_with_pending_changes.nhs_number.present? && + !patient_with_pending_changes.invalidated? + return + end + + pds_patient = + PDS::Patient.search( + family_name: patient_with_pending_changes.family_name, + given_name: patient_with_pending_changes.given_name, + date_of_birth: patient_with_pending_changes.date_of_birth, + address_postcode: patient_with_pending_changes.address_postcode + ) + + return if pds_patient.nil? + + patient.stage_changes( + nhs_number: pds_patient.nhs_number, + invalidated_at: nil + ) + end +end diff --git a/app/jobs/send_vaccination_confirmations_job.rb b/app/jobs/send_vaccination_confirmations_job.rb index 489ae1fcaf..718e78f46e 100644 --- a/app/jobs/send_vaccination_confirmations_job.rb +++ b/app/jobs/send_vaccination_confirmations_job.rb @@ -23,7 +23,7 @@ def perform .select { it.academic_year == academic_year } .each do |vaccination_record| send_vaccination_confirmation(vaccination_record) - vaccination_record.update!(confirmation_sent_at: Time.current) + vaccination_record.update_column(:confirmation_sent_at, Time.current) end end end diff --git a/app/lib/csv_parser.rb b/app/lib/csv_parser.rb index e43d3d362a..b21a586b99 100644 --- a/app/lib/csv_parser.rb +++ b/app/lib/csv_parser.rb @@ -107,13 +107,13 @@ def converters row = info.line header = unconverted_headers[info.index] - Field.new(value&.tr("\u00A0", " ")&.strip.presence, column, row, header) + Field.new(value&.normalise_whitespace, column, row, header) end end def header_converters proc do |value| - value.downcase.tr("-", "_").tr(" ", "_").tr("\u00A0", " ").strip.to_sym + value.downcase.normalise_whitespace.tr("-", "_").tr(" ", "_").to_sym end end end diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb index 99f92ccdef..24c20fadab 100644 --- a/app/lib/govuk_notify_personalisation.rb +++ b/app/lib/govuk_notify_personalisation.rb @@ -58,6 +58,8 @@ def to_h team_email:, team_name:, team_phone:, + team_phone_instructions_present:, + team_phone_instructions:, today_or_date_of_vaccination:, vaccination: }.compact @@ -245,6 +247,14 @@ def team_phone (team || organisation).phone end + def team_phone_instructions_present + team_phone_instructions.present? ? "yes" : "no" + end + + def team_phone_instructions + (team || organisation).phone_instructions + end + def today_or_date_of_vaccination return if vaccination_record.nil? diff --git a/app/lib/reports/careplus_exporter.rb b/app/lib/reports/careplus_exporter.rb index 3e23b2d938..371b279dab 100644 --- a/app/lib/reports/careplus_exporter.rb +++ b/app/lib/reports/careplus_exporter.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true class Reports::CareplusExporter + PROGRAMME_TYPE_TO_VACCINE_CODE = { + "flu" => "FLU", + "hpv" => "HPV", + "td_ipv" => "3IN1", + "menacwy" => "ACWYX4" + }.freeze + def initialize(organisation:, programme:, start_date:, end_date:) @organisation = organisation @programme = programme @@ -55,6 +62,7 @@ def headers def vaccine_columns(number) [ "Vaccine #{number}", + "Vaccine Code #{number}", "Dose #{number}", "Reason Not Given #{number}", "Site #{number}", @@ -154,6 +162,7 @@ def vaccine_fields(vaccination_records, index) [ record.vaccine.snomed_product_code, # Vaccine X + vaccine_code(record), # Code X field "#{record.dose_sequence}P", # Dose X field "", # Reason Not Given X coded_site(record.delivery_site), # Site X; Coded value @@ -202,4 +211,15 @@ def coded_site(site) # We don't implement the other codes currently }.fetch(site.to_sym) end + + def vaccine_code(vaccination_record) + code = + PROGRAMME_TYPE_TO_VACCINE_CODE.fetch(vaccination_record.programme.type) + + if code == "FLU" && vaccination_record.delivery_method == "nasal_spray" + return "FLUENZ" + end + + code + end end diff --git a/app/lib/reports/offline_session_exporter.rb b/app/lib/reports/offline_session_exporter.rb index 0a0fb95f6e..58160d352a 100644 --- a/app/lib/reports/offline_session_exporter.rb +++ b/app/lib/reports/offline_session_exporter.rb @@ -125,17 +125,27 @@ def columns end def patient_sessions - session - .patient_sessions - .includes( - patient: [ - :consent_statuses, - :school, - { vaccination_records: %i[batch performed_by_user vaccine] } - ], - session: :programmes - ) - .order_by_name + @patient_sessions ||= + session + .patient_sessions + .includes( + patient: [ + :consent_statuses, + :school, + { parent_relationships: :parent }, + { + vaccination_records: %i[ + batch + performed_by_user + vaccine + programme + session + ] + } + ], + session: [{ programmes: :vaccines }, :location] + ) + .order_by_name end def consents @@ -143,7 +153,7 @@ def consents Consent .where(patient_id: patient_sessions.select(:patient_id)) .not_invalidated - .includes(:parent, :patient) + .includes(:parent, patient: { parent_relationships: :parent }) .group_by(&:patient_id) .transform_values do it @@ -206,7 +216,9 @@ def rows(patient_session:) } vaccination_records = - patient.vaccination_records.select { it.programme_id == programme.id } + patient.vaccination_records.to_a.select do + it.programme_id == programme.id + end if vaccination_records.any? vaccination_records.map do |vaccination_record| @@ -409,7 +421,7 @@ def batch_numbers_range_for_programme(programme) end def clinic_name_values - @clinic_name_values = + @clinic_name_values ||= Location .community_clinic .joins(:team) diff --git a/app/lib/update_patients_from_pds.rb b/app/lib/update_patients_from_pds.rb index 6b7e52aa74..ddd70e3435 100644 --- a/app/lib/update_patients_from_pds.rb +++ b/app/lib/update_patients_from_pds.rb @@ -11,7 +11,9 @@ def call return unless enqueue? GoodJob::Bulk.enqueue do - patients.find_each.with_index do |patient, index| + jobs_queued = 0 + + patients.find_each do |patient| # Schedule with a delay to preemptively handle rate limit issues. # This shouldn't be necessary, but we're finding that Good Job # has occasional race condition issues, and spreading out the jobs @@ -21,14 +23,26 @@ def call PatientNHSNumberLookupJob.set( priority:, queue:, - wait: index * wait_between_jobs + wait: jobs_queued * wait_between_jobs ).perform_later(patient) else PatientUpdateFromPDSJob.set( priority:, queue:, - wait: index * wait_between_jobs + wait: jobs_queued * wait_between_jobs + ).perform_later(patient) + end + + jobs_queued += 1 + + if patient.pending_changes.present? + PatientNHSNumberLookupWithPendingChangesJob.set( + priority:, + queue:, + wait: jobs_queued * wait_between_jobs ).perform_later(patient) + + jobs_queued += 1 end end end diff --git a/app/models/batch.rb b/app/models/batch.rb index f2681278ca..398588957b 100644 --- a/app/models/batch.rb +++ b/app/models/batch.rb @@ -42,7 +42,15 @@ class Batch < ApplicationRecord scope :not_expired, -> { where.not(expiry: nil).where("expiry > ?", Time.current) } - validates :name, presence: true, format: { with: /\A[A-Za-z0-9]+\z/ } + validates :name, + presence: true, + format: { + with: /\A[A-Za-z0-9]+\z/ + }, + length: { + minimum: 2, + maximum: 100 + } validates :expiry, uniqueness: { diff --git a/app/models/consent_notification.rb b/app/models/consent_notification.rb index dd4fc4d6c2..42dd27d3a4 100644 --- a/app/models/consent_notification.rb +++ b/app/models/consent_notification.rb @@ -31,6 +31,8 @@ class ConsentNotification < ApplicationRecord belongs_to :patient belongs_to :session + has_one :organisation, through: :session + has_many :consent_notification_programmes, -> { joins(:programme).order(:"programmes.type") }, dependent: :destroy diff --git a/app/models/draft_consent.rb b/app/models/draft_consent.rb index 00a4a4e861..970f7f82d3 100644 --- a/app/models/draft_consent.rb +++ b/app/models/draft_consent.rb @@ -141,11 +141,13 @@ def new_or_existing_contact=(value) @new_or_existing_contact = value if value == "new" + self.route = nil self.parent = nil elsif value == "patient" self.route = "self_consent" self.parent = nil else + self.route = nil self.parent = patient.parents.find_by(id: value) || Parent.where(consents: patient.consents.where(programme:)).find_by( diff --git a/app/models/gillick_assessment.rb b/app/models/gillick_assessment.rb index adae366904..653ba9ee5c 100644 --- a/app/models/gillick_assessment.rb +++ b/app/models/gillick_assessment.rb @@ -54,6 +54,8 @@ class GillickAssessment < ApplicationRecord in: [true, false] } + validates :notes, length: { maximum: 1000 } + def gillick_competent? knows_consequences && knows_delivery && knows_disease && knows_side_effects && knows_vaccination diff --git a/app/models/immunisation_import_row.rb b/app/models/immunisation_import_row.rb index c64f7a34e4..3cc5d2842a 100644 --- a/app/models/immunisation_import_row.rb +++ b/app/models/immunisation_import_row.rb @@ -32,6 +32,8 @@ class ImmunisationImportRow CARE_SETTING_SCHOOL = 1 CARE_SETTING_COMMUNITY = 2 + MAX_FIELD_LENGTH = 300 + DELIVERY_SITES = { "left thigh" => "left_thigh", "right thigh" => "right_thigh", @@ -489,6 +491,8 @@ def validate_batch_name ) elsif batch_name.blank? errors.add(batch_name.header, "Enter a batch number.") + elsif batch_name.to_s.length > 100 + errors.add(batch_name.header, "is greater than 100 characters long") end end elsif batch_name.present? @@ -517,6 +521,11 @@ def validate_clinic_name ) elsif clinic_name.blank? errors.add(clinic_name.header, "Enter a clinic name") + elsif clinic_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + clinic_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) elsif !organisation.community_clinics.exists?(name: clinic_name.to_s) errors.add(clinic_name.header, "Enter a clinic name") end @@ -663,6 +672,11 @@ def validate_patient_first_name ) elsif patient_first_name.blank? errors.add(patient_first_name.header, "Enter a first name.") + elsif patient_first_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + patient_first_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) end end @@ -690,6 +704,11 @@ def validate_patient_last_name ) elsif patient_last_name.blank? errors.add(patient_last_name.header, "Enter a last name.") + elsif patient_last_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + patient_last_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) end end @@ -824,6 +843,11 @@ def validate_school_name else errors.add(school_name.header, "Enter a school name.") end + elsif school_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + school_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) end end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 45fd118cba..683f87a96c 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -13,6 +13,7 @@ # name :text not null # ods_code :string not null # phone :string +# phone_instructions :string # privacy_notice_url :string not null # privacy_policy_url :string not null # created_at :datetime not null @@ -68,7 +69,9 @@ def year_groups end def generic_team - teams.create_with(email:, phone:).find_or_create_by!(name:) + teams.create_with(email:, phone:, phone_instructions:).find_or_create_by!( + name: + ) end def generic_clinic diff --git a/app/models/patient/triage_status.rb b/app/models/patient/triage_status.rb index 9ec2216228..a49ed83377 100644 --- a/app/models/patient/triage_status.rb +++ b/app/models/patient/triage_status.rb @@ -62,7 +62,7 @@ def assign_status end def consent_requires_triage? - ConsentGrouper.call(consents, programme_id:).any?(&:triage_needed?) + latest_consents.any?(&:triage_needed?) end def vaccination_history_requires_triage? @@ -88,9 +88,24 @@ def status_should_be_delay_vaccination? def status_should_be_required? return true if latest_triage&.needs_follow_up? + return false if latest_consents.empty? + + consent_given = + if (self_consents = latest_consents.select(&:via_self_consent?)).any? + self_consents.all?(&:response_given?) + else + latest_consents.all?(&:response_given?) + end + + return false unless consent_given + consent_requires_triage? || vaccination_history_requires_triage? end + def latest_consents + @latest_consents ||= ConsentGrouper.call(consents, programme_id:) + end + def latest_triage @latest_triage ||= triages.find { it.programme_id == programme_id } end diff --git a/app/models/patient_import_row.rb b/app/models/patient_import_row.rb index 716afa1f5d..5c6bf0352f 100644 --- a/app/models/patient_import_row.rb +++ b/app/models/patient_import_row.rb @@ -3,6 +3,8 @@ class PatientImportRow include ActiveModel::Model + MAX_FIELD_LENGTH = 300 + validate :validate_date_of_birth, :validate_existing_patients, :validate_first_name, @@ -47,6 +49,27 @@ def to_patient existing_patient.registration = attributes.delete(:registration) end + auto_accept_attribute( + existing_patient, + attributes, + :gender_code, + :in?, + %w[male female not_specified] + ) + + auto_accept_attribute( + existing_patient, + attributes, + :preferred_given_name, + :present? + ) + auto_accept_attribute( + existing_patient, + attributes, + :preferred_family_name, + :present? + ) + if address_postcode.present? && address_postcode.to_postcode != existing_patient.address_postcode attributes.merge!( @@ -54,6 +77,10 @@ def to_patient address_line_2: address_line_2&.to_s, address_town: address_town&.to_s ) + elsif auto_overwrite_address?(existing_patient) + existing_patient.address_line_1 = attributes.delete(:address_line_1) + existing_patient.address_line_2 = attributes.delete(:address_line_2) + existing_patient.address_town = attributes.delete(:address_town) end existing_patient.stage_changes(attributes) @@ -195,6 +222,27 @@ def nhs_number_value private + def auto_accept_attribute( + existing_patient, + attributes, + attribute_name, + condition_function, + *condition_params + ) + if attributes[attribute_name].send(condition_function, *condition_params) && + !existing_patient[attribute_name].send( + condition_function, + *condition_params + ) + existing_patient[attribute_name] = attributes.delete(attribute_name) + end + end + + def auto_overwrite_address?(existing_patient) + existing_patient.address_postcode == address_postcode&.to_postcode && + [address_line_1, address_line_2, address_town].any?(&:present?) + end + def parent_1_exists? [parent_1_name, parent_1_email, parent_1_phone].any?(&:present?) end @@ -284,6 +332,11 @@ def validate_first_name errors.add(:base, "CHILD_FIRST_NAME is missing") elsif first_name.blank? errors.add(first_name.header, "is required but missing") + elsif first_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + first_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) end end @@ -299,6 +352,11 @@ def validate_last_name errors.add(:base, "CHILD_LAST_NAME is missing") elsif last_name.blank? errors.add(last_name.header, "is required but missing") + elsif last_name.to_s.length > MAX_FIELD_LENGTH + errors.add( + last_name.header, + "is greater than #{MAX_FIELD_LENGTH} characters long" + ) end end diff --git a/app/models/pre_screening.rb b/app/models/pre_screening.rb index 5da9018a5d..a6c537b75a 100644 --- a/app/models/pre_screening.rb +++ b/app/models/pre_screening.rb @@ -46,4 +46,6 @@ class PreScreening < ApplicationRecord has_one :patient, through: :patient_session encrypts :notes + + validates :notes, length: { maximum: 1000 } end diff --git a/app/models/team.rb b/app/models/team.rb index e01f67c368..8feb81fca1 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -4,14 +4,15 @@ # # Table name: teams # -# id :bigint not null, primary key -# email :string not null -# name :string not null -# phone :string not null -# created_at :datetime not null -# updated_at :datetime not null -# organisation_id :bigint not null -# reply_to_id :uuid +# id :bigint not null, primary key +# email :string not null +# name :string not null +# phone :string not null +# phone_instructions :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# reply_to_id :uuid # # Indexes # diff --git a/app/policies/consent_notification_policy.rb b/app/policies/consent_notification_policy.rb new file mode 100644 index 0000000000..3d0b556bb3 --- /dev/null +++ b/app/policies/consent_notification_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ConsentNotificationPolicy < ApplicationPolicy + class Scope < ApplicationPolicy::Scope + def resolve + scope.joins(:session).where( + session: { + organisation: user.selected_organisation + } + ) + end + end +end diff --git a/app/views/organisations/show.html.erb b/app/views/organisations/show.html.erb index d88bd61955..ab101d0891 100644 --- a/app/views/organisations/show.html.erb +++ b/app/views/organisations/show.html.erb @@ -14,7 +14,7 @@ summary_list.with_row do |row| row.with_key { "Phone number" } - row.with_value { @organisation.phone } + row.with_value { format_phone_with_instructions(@organisation) } end end %> <% end %> diff --git a/app/views/parent_interface/consent_forms/cannot_consent_responsibility.html.erb b/app/views/parent_interface/consent_forms/cannot_consent_responsibility.html.erb index 74e2b81941..1d2fdb9fcf 100644 --- a/app/views/parent_interface/consent_forms/cannot_consent_responsibility.html.erb +++ b/app/views/parent_interface/consent_forms/cannot_consent_responsibility.html.erb @@ -14,5 +14,5 @@

If you have any questions, please contact the local health organisation by calling - <%= @team.phone %>, or email <%= mail_to @team.email %>. + <%= format_phone_with_instructions(@team) %>, or email <%= mail_to @team.email %>.

diff --git a/app/views/programmes/show.html.erb b/app/views/programmes/show.html.erb index b743b892ae..6b854fa63f 100644 --- a/app/views/programmes/show.html.erb +++ b/app/views/programmes/show.html.erb @@ -13,27 +13,5 @@ <%= govuk_button_to "Download vaccination report", programme_vaccination_reports_path(@programme), class: "app-button--secondary nhsuk-u-margin-bottom-5" %> -
-
- <%= render AppCardComponent.new(link_to: programme_cohorts_path(@programme), colour: "reversed", data: true) do |card| %> - <% card.with_heading { "Children" } %> - <% card.with_description { @patients_count.to_s } %> - <% end %> -
- -
- <%= render AppCardComponent.new(link_to: programme_vaccination_records_path(@programme), colour: "reversed", data: true) do |card| %> - <% card.with_heading { "Vaccinations" } %> - <% card.with_description { @vaccinations_count.to_s } %> - <% end %> -
- -
- <%= render AppCardComponent.new(data: true, colour: "reversed") do |card| %> - <% card.with_heading { "Consent requests and reminders sent" } %> - <% card.with_description { @consent_notifications_count.to_s } %> - <% end %> -
-
- +<%= render AppProgrammeStatsComponent.new(programme: @programme) %> <%= render AppConsentRefusedTableComponent.new(@consents) %> diff --git a/app/views/school_moves/index.html.erb b/app/views/school_moves/index.html.erb index e038c1e4c8..0bba132921 100644 --- a/app/views/school_moves/index.html.erb +++ b/app/views/school_moves/index.html.erb @@ -1,13 +1,10 @@ <%= h1 "School moves", size: "xl" %> -
-
-

When imported records or a new consent response indicates that a child has changed - school, Mavis flags this as a school move.

-

You can then review the new information and confirm the school move or ignore it.

+
+

When imported records or a new consent response indicates that a child has changed school, Mavis flags this as a school move.

+

You can then review the new information and confirm the school move or ignore it.

- <%= govuk_button_to "Download records", school_move_exports_path, class: "app-button--secondary nhsuk-u-margin-bottom-5" %> -
+ <%= govuk_button_to "Download records", school_move_exports_path, class: "app-button--secondary nhsuk-u-margin-bottom-5" %>
<% if @school_moves.any? %> diff --git a/app/views/users/errors/organisation_not_found.html.erb b/app/views/users/errors/organisation_not_found.html.erb index f65b5fdceb..2a04a5bece 100644 --- a/app/views/users/errors/organisation_not_found.html.erb +++ b/app/views/users/errors/organisation_not_found.html.erb @@ -9,6 +9,8 @@ (<%= @cis2_info[:selected_org][:code] %>) <% end %> +

You'll be able to use Mavis once your organisation has been onboarded.

+ <% if @cis2_info[:has_other_roles] %> <%= govuk_button_to "Change role", user_cis2_omniauth_authorize_path, params: { change_role: true } %> <% end %> diff --git a/config/initializers/semantic_logger.rb b/config/initializers/semantic_logger.rb index 0f8788db46..ae69614398 100644 --- a/config/initializers/semantic_logger.rb +++ b/config/initializers/semantic_logger.rb @@ -13,7 +13,10 @@ def initialize(request_channel: nil, **args, &block) class MavisSplunkFormatter def call(log, logger) message = JSON.parse(logger.call(log, logger)) - message["event"]["hosting_environment"] = HostingEnvironment.name + message["time"] = message["time"].floor(6) + if defined?(HostingEnvironment) + message["event"]["hosting_environment"] = HostingEnvironment.name + end message.to_json end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 7fc811b512..5edb661ebe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,6 +16,8 @@ en: name: blank: Enter a batch invalid: Enter a batch with only letters and numbers + too_short: Enter a batch that is more than %{count} characters long + too_long: Enter a batch that is less than %{count} characters long draft_class_import: attributes: session_id: @@ -332,6 +334,8 @@ en: inclusion: Choose whether the child knows how the injection will be given knows_side_effects: inclusion: Choose whether the child knows which side effects they might experience + notes: + too_long: Enter notes that are less than %{count} characters long immunisation_import: attributes: csv: @@ -359,6 +363,10 @@ en: invalid: Enter a valid NHS number wrong_length: Enter a valid NHS number with 10 characters taken: NHS number is already assigned to a different patient + pre_screening: + attributes: + notes: + too_long: Enter notes that are less than %{count} characters long programme: attributes: type: diff --git a/db/migrate/20250429081203_add_phone_instructions_to_organisations_and_teams.rb b/db/migrate/20250429081203_add_phone_instructions_to_organisations_and_teams.rb new file mode 100644 index 0000000000..03e5043794 --- /dev/null +++ b/db/migrate/20250429081203_add_phone_instructions_to_organisations_and_teams.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddPhoneInstructionsToOrganisationsAndTeams < ActiveRecord::Migration[8.0] + def change + add_column :organisations, :phone_instructions, :string + add_column :teams, :phone_instructions, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1b54f2e362..41b342f717 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -509,6 +509,7 @@ t.integer "days_before_invitations", default: 21, null: false t.string "careplus_venue_code", null: false t.string "privacy_notice_url", null: false + t.string "phone_instructions" t.index ["name"], name: "index_organisations_on_name", unique: true t.index ["ods_code"], name: "index_organisations_on_ods_code", unique: true end @@ -735,6 +736,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.uuid "reply_to_id" + t.string "phone_instructions" t.index ["organisation_id", "name"], name: "index_teams_on_organisation_id_and_name", unique: true end diff --git a/lib/core_ext/string/normalise_whitespace.rb b/lib/core_ext/string/normalise_whitespace.rb new file mode 100644 index 0000000000..a8952b425d --- /dev/null +++ b/lib/core_ext/string/normalise_whitespace.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class String + def normalise_whitespace + result = self + + # \u200D is a zero-width joiner (ZWJ) which is used in the frontend to display the NHS number + result = result.tr("\u200D", "") + + # \u00A0 is a non-breaking space + result = result.tr("\u00A0", " ") + + result.strip.gsub(/\s+/, " ").presence + end +end diff --git a/lib/generate/patient_imports.rb b/lib/generate/cohort_imports.rb similarity index 64% rename from lib/generate/patient_imports.rb rename to lib/generate/cohort_imports.rb index 2fa424a8ba..59068cf733 100644 --- a/lib/generate/patient_imports.rb +++ b/lib/generate/cohort_imports.rb @@ -8,17 +8,41 @@ # # Usage from the Rails console: # -# # By default it uses existing locations in the db. -# Generate::PatientImports.call +# Create a cohort import of 1000 children for all the school sessions for the +# org A9A5A in the local db: +# +# Generate::CohortImports.call(patient_count: 1000) +# +# You can also generate a cohort import for sessions not in the local db. +# +# Generate::CohortImports.call( +# patient_count: 1000, +# urns: ["123456", "987654"], +# school_year_groups: { +# "123456" => [-2, -1, 0, 1, 2, 3, 4, 5, 6], +# "987654" => [9, 10, 11, 12, 13] +# } +# ) +# +# You can pull out the year groups with the following: +# +# org = Organisation.find_by(ods_code: "A9A5A") +# org.locations.school.pluck(:urn, :year_groups) .to_h # module Generate - class PatientImports - attr_reader :ods_code, :organisation, :programme, :urns, :patient_count + class CohortImports + attr_reader :ods_code, + :organisation, + :programme, + :urns, + :patient_count, + :school_year_groups def initialize( ods_code: "A9A5A", programme: "hpv", urns: nil, + school_year_groups: nil, patient_count: 10 ) @organisation = Organisation.find_by(ods_code:) @@ -30,19 +54,19 @@ def initialize( .select { it.urn.present? } .sample(3) .pluck(:urn) + @school_year_groups = school_year_groups @patient_count = patient_count + @nhs_numbers = Set.new end def self.call(...) = new(...).call def call - generate write_cohort_import_csv - write_class_import_csv end def patients - @patients = patient_count.times.map { build_patient } + patient_count.times.lazy.map { build_patient } end private @@ -53,12 +77,6 @@ def cohort_import_csv_filepath ) end - def class_import_csv_filepath(school:) - Rails.root.join( - "tmp/perf-test-class-import-#{school.name}-#{school.sessions.first.slug}.csv" - ) - end - def write_cohort_import_csv CSV.open(cohort_import_csv_filepath, "w") do |csv| csv << %w[ @@ -105,40 +123,7 @@ def write_cohort_import_csv ] end end - end - - def write_class_import_csv - patients - .group_by(&:school) - .each do |school, school_patients| - next if school.nil? - - CSV.open(class_import_csv_filepath(school:), "w") do |csv| - csv << %w[ - CHILD_POSTCODE - CHILD_DATE_OF_BIRTH - CHILD_FIRST_NAME - CHILD_LAST_NAME - PARENT_1_EMAIL - PARENT_1_PHONE - PARENT_2_EMAIL - PARENT_2_PHONE - ] - - school_patients.each do |patient| - csv << [ - patient.address_postcode, - patient.date_of_birth, - patient.given_name, - patient.family_name, - patient.parents.first&.email, - patient.parents.first&.phone, - patient.parents.second&.email, - patient.parents.second&.phone - ] - end - end - end + cohort_import_csv_filepath.to_s end def programme_year_groups @@ -147,28 +132,46 @@ def programme_year_groups def schools_with_year_groups @schools_with_year_groups ||= - organisation - .locations - .includes(:organisation, :sessions) - .select { (it.year_groups & programme_year_groups).any? } + begin + locations = + if school_year_groups.present? + urns.map do |urn| + Location.new(urn:, year_groups: school_year_groups[urn]) + end + else + organisation + .locations + .where(urn: urns) + .includes(:organisation, :sessions) + end + locations.select { (it.year_groups & programme_year_groups).any? } + end end - def build_patient(year_group: nil) + def build_patient school = schools_with_year_groups.sample year_group ||= (school.year_groups & programme_year_groups).sample + nhs_number = nil + loop do + nhs_number = Faker::NationalHealthService.british_number.gsub(" ", "") + break unless nhs_number.in? @nhs_numbers + end + @nhs_numbers << nhs_number FactoryBot .build( :patient, school:, - date_of_birth: date_of_birth_for_year(year_group) + organisation:, + date_of_birth: date_of_birth_for_year(year_group), + nhs_number: ) .tap do |patient| patient.parents = FactoryBot.build_list(:parent, 2, family_name: patient.family_name) patient.parent_relationships = patient.parents.map do - FactoryBot.build(:parent_relationship, parent: it) + FactoryBot.build(:parent_relationship, parent: it, patient:) end end end diff --git a/lib/generate/consents.rb b/lib/generate/consents.rb index d4e86bd13c..e0dea8a797 100644 --- a/lib/generate/consents.rb +++ b/lib/generate/consents.rb @@ -12,6 +12,8 @@ def initialize( given: 0, given_needs_triage: 0 ) + validate_programme_and_session(programme, session) if programme + @organisation = organisation @programme = programme || organisation.programmes.sample @session = session @@ -24,6 +26,7 @@ def call create_consent_with_response(:refused, @refused) create_consent_with_response(:given, @given) create_consent_given_needs_triage(@given_needs_triage) + StatusUpdater.call(patient: patients) end def self.call(...) = new(...).call @@ -31,11 +34,26 @@ def self.call(...) = new(...).call private def patients - (@session.presence || organisation) - .patients - .includes(:parents, :consents, consents: :parent) - .in_programmes([programme]) - .select { it.consents.empty? && it.parents.any? } + @patients ||= + begin + sessions = + if @session + [@session] + else + organisation + .sessions + .eager_load(:location) + .merge(Location.school) + .has_programme(programme) + end + + sessions.flat_map do |session| + session + .patients + .includes(:parents, :school, :consents, consents: :parent) + .select { it.consents.empty? && it.parents.any? } + end + end end def random_patients(count) @@ -50,12 +68,13 @@ def random_patients(count) end def session_for(patient) - patient - .sessions - .eager_load(:location) - .merge(Location.school) - .has_programme(programme) - .sample + @session || + patient + .sessions + .eager_load(:location) + .merge(Location.school) + .has_programme(programme) + .sample end def create_consent_with_response(response, count) @@ -64,11 +83,13 @@ def create_consent_with_response(response, count) available_patient_sessions.each do |patient, session| consent = FactoryBot.create(:consent, response, patient:, programme:) + school = session.location.school? ? session.location : patient.school FactoryBot.create( :consent_form, organisation:, programmes: [programme], session:, + school:, consent:, response: ) @@ -98,5 +119,15 @@ def create_consent_given_needs_triage(count) ) end end + + def validate_programme_and_session(programme, session) + if session + if session.programmes.exclude?(programme) + raise "Session does not support programme #{programme.type}" + end + elsif programme.sessions.none? { it.location.school? } + raise "Programme #{programme.type} does not have a school session" + end + end end end diff --git a/lib/generate/vaccination_records.rb b/lib/generate/vaccination_records.rb index 2cb041d5f3..66627a96f3 100644 --- a/lib/generate/vaccination_records.rb +++ b/lib/generate/vaccination_records.rb @@ -2,9 +2,14 @@ module Generate class VaccinationRecords - attr_reader :config, :organisation, :programme + attr_reader :config, :organisation, :programme, :session, :administered - def initialize(organisation:, programme: nil, session: nil, administered: 0) + def initialize( + organisation:, + programme: nil, + session: nil, + administered: nil + ) @organisation = organisation @programme = programme || organisation.programmes.sample @session = session @@ -12,19 +17,65 @@ def initialize(organisation:, programme: nil, session: nil, administered: 0) end def call - create_vaccination_administered(@administered) + create_vaccinations end def self.call(...) = new(...).call private + def create_vaccinations + random_patient_sessions.each do |patient_session| + patient_session_id = patient_session.id + session_date_ids = patient_session.session.session_dates.pluck(:id) + + unless SessionAttendance.exists?( + patient_session_id:, + session_date_id: session_date_ids + ) + FactoryBot.create(:session_attendance, :present, patient_session:) + end + + FactoryBot.create( + :vaccination_record, + :administered, + patient: patient_session.patient, + programme:, + performed_by:, + session:, + vaccine:, + batch:, + location_name: patient_session.location.name + ) + end + + StatusUpdater.call(patient: patient_sessions.map(&:patient)) + end + + def random_patient_sessions + if administered&.positive? + patient_sessions + .shuffle + .take(administered) + .tap do |selected| + if selected.size < administered + info = + "#{selected.size} (patient_sessions) < #{administered} (administered)" + raise "Not enough patients to generate vaccinations: #{info}" + end + end + else + patient_sessions + end + end + def patient_sessions - (@session.presence || organisation) + (session.presence || organisation) .patient_sessions .joins(:patient) .includes( :session, + :location, patient: [ :consents, :triages, @@ -38,50 +89,15 @@ def patient_sessions end def vaccine - @vaccine ||= programme.vaccines.includes(:batches).active.first + programme.vaccines.includes(:batches).active.first end def batch - @batch ||= vaccine.batches.sample + vaccine.batches.sample end - def random_patient_sessions(count) - patient_sessions - .shuffle - .take(count) - .tap do - if it.size < count - raise "Not enough patients to generate vaccinations" - end - end - end - - def location_name(session) - session.location.generic_clinic? ? session.location.name : "" - end - - def user - @user ||= organisation.users.includes(:organisations).sample - end - - def create_vaccination_administered(count) - available_patient_sessions = random_patient_sessions(count) - - available_patient_sessions.each do |patient_session| - FactoryBot.create(:session_attendance, :present, patient_session:) - - FactoryBot.create( - :vaccination_record, - :administered, - patient: patient_session.patient, - programme:, - performed_by: user, - session: patient_session.session, - vaccine:, - batch:, - location_name: location_name(patient_session.session) - ) - end + def performed_by + organisation.users.includes(:organisations).sample end end end diff --git a/lib/omni_auth_strategies_openid_connect_patch.rb b/lib/omni_auth_strategies_openid_connect_patch.rb index 4cd3124425..65da3e7d17 100644 --- a/lib/omni_auth_strategies_openid_connect_patch.rb +++ b/lib/omni_auth_strategies_openid_connect_patch.rb @@ -6,9 +6,11 @@ def access_token token_request_params = { scope: (options.scope if options.send_scope_to_token_endpoint), - client_auth_method: options.client_auth_method, - client_assertion: generate_client_assertion + client_auth_method: options.client_auth_method } + if client_options.key?(:private_key) + token_request_params[:client_assertion] = generate_client_assertion + end token_request_params[:code_verifier] = params["code_verifier"] || session.delete("omniauth.pkce.verifier") if options.pkce diff --git a/package.json b/package.json index a77aa41752..6eeeedec22 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.13", "accessible-autocomplete": "^3.0.1", - "esbuild": "^0.25.3", + "esbuild": "^0.25.4", "govuk-frontend": "^5.10.0", - "idb": "^8.0.2", + "idb": "^8.0.3", "nhsuk-frontend": "^9.4.1", - "sass": "^1.87.0", + "sass": "^1.88.0", "stimulus": "^3.2.2", "workbox-build": "^7.3.0" }, diff --git a/spec/factories/locations.rb b/spec/factories/locations.rb index 379e17ae70..aff8146b59 100644 --- a/spec/factories/locations.rb +++ b/spec/factories/locations.rb @@ -45,7 +45,11 @@ url { Faker::Internet.url } - team { organisation ? association(:team, organisation:) : nil } + team do + if organisation + organisation.teams.first || association(:team, organisation:) + end + end traits_for_enum :status diff --git a/spec/factories/organisations.rb b/spec/factories/organisations.rb index 0649a52e8e..c6a4617218 100644 --- a/spec/factories/organisations.rb +++ b/spec/factories/organisations.rb @@ -13,6 +13,7 @@ # name :text not null # ods_code :string not null # phone :string +# phone_instructions :string # privacy_notice_url :string not null # privacy_policy_url :string not null # created_at :datetime not null diff --git a/spec/factories/patients.rb b/spec/factories/patients.rb index df2d8110b2..bcca41bbe6 100644 --- a/spec/factories/patients.rb +++ b/spec/factories/patients.rb @@ -63,6 +63,7 @@ year_group { programmes.flat_map(&:year_groups).sort.uniq.first } location_name { nil } in_attendance { false } + random_nhs_number { false } end organisation do @@ -71,15 +72,20 @@ end nhs_number do - # Prevents duplicate NHS numbers by sequencing and appending a check - # digit. See Faker's implementation for details: - # https://github.com/faker-ruby/faker/blob/6ba06393f47d4018b5fdbdaaa04eb9891ae5fb55/lib/faker/default/national_health_service.rb - base = 999_000_000 + generate(:nhs_number_counter) - sum = base.to_s.chars.map.with_index { |d, i| d.to_i * (10 - i) }.sum - check_digit = (11 - (sum % 11)) % 11 - redo if check_digit == 10 # Retry if check digit is 10, which is invalid - - "#{base}#{check_digit}" + if random_nhs_number + Faker::NationalHealthService.british_number.gsub(" ", "") + else + # Faker doesn't allow us to generate sequential NHS numbers, so this is + # reimplemented here. + # + # https://github.com/faker-ruby/faker/blob/6ba06393f47d4018b5fdbdaaa04eb9891ae5fb55/lib/faker/default/national_health_service.rb + base = 999_000_000 + generate(:nhs_number_counter) + sum = base.to_s.chars.map.with_index { |d, i| d.to_i * (10 - i) }.sum + check_digit = (11 - (sum % 11)) % 11 + redo if check_digit == 10 # Retry if check digit is 10, which is invalid + + "#{base}#{check_digit}" + end end given_name { Faker::Name.first_name } diff --git a/spec/factories/teams.rb b/spec/factories/teams.rb index 78687fc850..47e89ccb5b 100644 --- a/spec/factories/teams.rb +++ b/spec/factories/teams.rb @@ -4,14 +4,15 @@ # # Table name: teams # -# id :bigint not null, primary key -# email :string not null -# name :string not null -# phone :string not null -# created_at :datetime not null -# updated_at :datetime not null -# organisation_id :bigint not null -# reply_to_id :uuid +# id :bigint not null, primary key +# email :string not null +# name :string not null +# phone :string not null +# phone_instructions :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# reply_to_id :uuid # # Indexes # diff --git a/spec/features/cohorts_index_spec.rb b/spec/features/cohorts_index_spec.rb new file mode 100644 index 0000000000..0e8f4d1b28 --- /dev/null +++ b/spec/features/cohorts_index_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +describe "Cohorts index" do + around { |example| travel_to(Date.new(2024, 5, 20)) { example.run } } + + scenario "Viewing cohorts for a programme" do + given_an_hpv_programme_is_underway + and_there_are_patients_in_different_year_groups + when_i_visit_the_cohorts_page + then_i_should_see_the_cohorts_in_the_correct_order + and_i_should_see_the_correct_patient_counts + and_the_cards_should_be_clickable_when_there_are_patients + and_the_cards_should_not_be_clickable_when_there_are_no_patients + end + + def given_an_hpv_programme_is_underway + @programme = create(:programme, :hpv) + @organisation = + create(:organisation, :with_one_nurse, programmes: [@programme]) + sign_in @organisation.users.first + end + + def and_there_are_patients_in_different_year_groups + # Create patients in year 8 and 9 + # For year 8 in 2024, birth academic year is 2010 + # For year 9 in 2024, birth academic year is 2009 + create( + :patient, + organisation: @organisation, + date_of_birth: Date.new(2010, 9, 1) + ) + create( + :patient, + organisation: @organisation, + date_of_birth: Date.new(2010, 9, 1) + ) + create( + :patient, + organisation: @organisation, + date_of_birth: Date.new(2009, 9, 1) + ) + end + + def when_i_visit_the_cohorts_page + visit "/dashboard" + click_on "Programmes", match: :first + click_on "HPV" + click_on "Cohort" + end + + def then_i_should_see_the_cohorts_in_the_correct_order + # Get all the cohort cards and check their order + cohort_cards = page.all(".nhsuk-card-group__item") + expect(cohort_cards[0]).to have_content("Year 8") + expect(cohort_cards[1]).to have_content("Year 9") + expect(cohort_cards[2]).to have_content("Year 10") + expect(cohort_cards[3]).to have_content("Year 11") + end + + def and_i_should_see_the_correct_patient_counts + expect(page).to have_content("Year 8\n2 children") + expect(page).to have_content("Year 9\n1 child") + expect(page).to have_content("Year 10\nNo children") + expect(page).to have_content("Year 11\nNo children") + end + + def and_the_cards_should_be_clickable_when_there_are_patients + # Year 8 and 9 cards should be clickable + expect(page).to have_link( + "Year 8", + href: programme_cohort_path(@programme, 2010) + ) + expect(page).to have_link( + "Year 9", + href: programme_cohort_path(@programme, 2009) + ) + end + + def and_the_cards_should_not_be_clickable_when_there_are_no_patients + # Year 10 and 11 cards should not be clickable + expect(page).not_to have_link( + "Year 10", + href: programme_cohort_path(@programme, 2008) + ) + expect(page).not_to have_link( + "Year 11", + href: programme_cohort_path(@programme, 2007) + ) + end +end diff --git a/spec/features/import_child_records_with_twins_spec.rb b/spec/features/import_child_records_with_twins_spec.rb new file mode 100644 index 0000000000..ff6b26de59 --- /dev/null +++ b/spec/features/import_child_records_with_twins_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +describe "Child record imports twins" do + around { |example| travel_to(Date.new(2024, 12, 1)) { example.run } } + + scenario "User reviews and selects between duplicate records" do + stub_pds_get_nhs_number_to_return_a_patient + stub_pds_search_to_return_a_patient + + given_i_am_signed_in + and_an_hpv_programme_is_underway + and_an_existing_patient_record_exists + + when_i_visit_the_cohort_page_for_the_hpv_programme + and_i_start_adding_children_to_the_cohort + and_i_upload_a_file_with_duplicate_records + then_i_should_see_the_import_page_with_duplicate_records + then_i_wait_for_the_background_job_to_complete + + when_i_review_the_duplicate_record + then_i_should_see_the_duplicate_record + + when_i_choose_to_keep_both_records + and_i_confirm_my_selection + then_i_should_see_a_success_message + and_a_new_patient_record_should_be_created + end + + def given_i_am_signed_in + @organisation = create(:organisation, :with_one_nurse) + sign_in @organisation.users.first + end + + def and_an_hpv_programme_is_underway + programme = create(:programme, :hpv, organisations: [@organisation]) + + @school = create(:school, urn: "123456", organisation: @organisation) + @session = + create( + :session, + organisation: @organisation, + location: @school, + programmes: [programme] + ) + end + + def and_an_existing_patient_record_exists + @existing_patient = + create( + :patient, + given_name: "John", + family_name: "Doe", + nhs_number: "9000000009", + date_of_birth: Date.new(2010, 1, 3), + gender_code: :male, + address_line_1: "11 Downing Street", + address_line_2: "", + address_town: "London", + address_postcode: "SW1A 1AA", + school: @school, + organisation: @organisation, + session: @session + ) + end + + def when_i_visit_the_cohort_page_for_the_hpv_programme + visit "/" + click_link "Programmes", match: :first + click_link "HPV" + click_link "Cohorts" + end + + def and_i_start_adding_children_to_the_cohort + click_link "Import child records" + end + + def and_i_upload_a_file_with_duplicate_records + attach_file("cohort_import[csv]", "spec/fixtures/cohort_import/valid.csv") + click_on "Continue" + end + + def then_i_should_see_the_import_page_with_duplicate_records + expect(page).to have_content("1 duplicate record needs review") + end + + def then_i_wait_for_the_background_job_to_complete + perform_enqueued_jobs + end + + def when_i_review_the_duplicate_record + click_on "Review DOE, John" + end + + def then_i_should_see_the_duplicate_record + expect(page).to have_content("This record needs reviewing") + expect(page).to have_content("NHS number944 ‍930 ‍6168\nFull nameDOE, Mark") + expect(page).to have_content("NHS number900 ‍000 ‍0009\nFull nameDOE, John") + end + + def when_i_choose_to_keep_both_records + choose "Keep both records" + end + + def and_i_confirm_my_selection + click_on "Resolve duplicate" + end + + def then_i_should_see_a_success_message + expect(page).to have_content("Record updated") + end + + def and_a_new_patient_record_should_be_created + expect(Patient.count).to eq(2) + + patient = Patient.last + expect(patient.address_postcode).to eq("SW1A 1AA") + expect(patient.date_of_birth).to eq(Date.new(2010, 1, 3)) + expect(patient.family_name).to eq("Doe") + expect(patient.gender_code).to eq("male") + expect(patient.given_name).to eq("Mark") + expect(patient.nhs_number).to eq("9449306168") + expect(patient.pending_changes).to eq({}) + expect(patient.school).to eq(@school) + expect(patient.sessions.count).to eq(2) + + session = patient.sessions.first + expect(session).to eq(@session) + end +end diff --git a/spec/features/parental_consent_closed_spec.rb b/spec/features/parental_consent_closed_spec.rb index 1609947fb6..008cc2527f 100644 --- a/spec/features/parental_consent_closed_spec.rb +++ b/spec/features/parental_consent_closed_spec.rb @@ -7,6 +7,15 @@ then_i_see_that_consent_is_closed end + scenario "Before parent submits the consent" do + given_an_hpv_programme_is_starting_soon + when_i_go_through_the_consent_journey + then_i_see_the_confirmation_page + + when_i_wait_a_long_time_before_submitting + then_i_see_that_consent_is_closed + end + def given_an_hpv_programme_is_underway_with_a_backfilled_session @programme = create(:programme, :hpv) @organisation = @@ -23,10 +32,84 @@ def given_an_hpv_programme_is_underway_with_a_backfilled_session ) end + def given_an_hpv_programme_is_starting_soon + @programme = create(:programme, :hpv) + @organisation = + create(:organisation, :with_one_nurse, programmes: [@programme]) + location = create(:school, name: "Pilot School", team: create(:team)) + @session = + create( + :session, + :scheduled, + organisation: @organisation, + programmes: [@programme], + location:, + date: Date.tomorrow + ) + @child = create(:patient, :consent_no_response, session: @session) + end + def when_i_go_to_the_consent_form visit start_parent_interface_consent_forms_path(@session, @programme) end + def when_i_go_through_the_consent_journey + visit start_parent_interface_consent_forms_path(@session, @programme) + + click_on "Start now" + + expect(page).to have_content("What is your child’s name?") + fill_in "First name", with: @child.given_name + fill_in "Last name", with: @child.family_name + choose "No" # Do they use a different name in school? + click_on "Continue" + + expect(page).to have_content("What is your child’s date of birth?") + fill_in "Day", with: @child.date_of_birth.day + fill_in "Month", with: @child.date_of_birth.month + fill_in "Year", with: @child.date_of_birth.year + click_on "Continue" + + choose "Yes, they go to this school" + click_on "Continue" + + expect(page).to have_content("About you") + fill_in "Full name", with: "Jane #{@child.family_name}" + choose "Mum" # Your relationship to the child + fill_in "Email address", with: "jane@example.com" + fill_in "Phone number", with: "07123456789" + check "Tick this box if you’d like to get updates by text message" + click_on "Continue" + + expect(page).to have_content("Phone contact method") + choose "I do not have specific needs" + click_on "Continue" + + expect(page).to have_content("Do you agree") + choose "Yes, I agree" + click_on "Continue" + + expect(page).to have_content("Home address") + fill_in "Address line 1", with: "1 Test Street" + fill_in "Address line 2 (optional)", with: "2nd Floor" + fill_in "Town or city", with: "Testville" + fill_in "Postcode", with: "TE1 1ST" + click_on "Continue" + + until page.has_content?("Check and confirm") + choose "No" + click_on "Continue" + end + end + + def then_i_see_the_confirmation_page + expect(page).to have_content("Check and confirm") + end + + def when_i_wait_a_long_time_before_submitting + travel_to(1.day.from_now) { click_on "Confirm" } + end + def then_i_see_that_consent_is_closed expect(page).to have_content("The deadline for responding has passed") end diff --git a/spec/features/self_consent_spec.rb b/spec/features/self_consent_spec.rb index 1cff54c428..05f837b0f7 100644 --- a/spec/features/self_consent_spec.rb +++ b/spec/features/self_consent_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe "Self-consent" do - scenario "From Gillick assessment" do + scenario "after Gillick assessment" do given_an_hpv_programme_is_underway and_there_is_a_child_without_parental_consent @@ -15,18 +15,35 @@ then_the_details_of_the_gillick_competence_assessment_are_visible and_the_activity_log_shows_the_gillick_non_competence and_the_activity_log_shows_the_gillick_competence + and_the_nurse_records_consent_for_the_child and_the_child_can_give_their_own_consent_that_the_nurse_records when_the_nurse_views_the_childs_record - then_they_see_that_the_child_has_consent + then_they_see_that_the_child_has_consent_from_themselves and_the_child_should_be_safe_to_vaccinate and_enqueued_jobs_run_with_no_errors end + scenario "change to parent consent" do + given_an_hpv_programme_is_underway + and_there_is_a_child_with_gillick_competence + and_the_child_has_a_parent + + when_the_nurse_goes_to_the_child + and_the_nurse_records_consent_for_the_child + and_changes_the_response_method_to_the_parent + then_the_parent_can_give_consent + + when_the_nurse_views_the_childs_record + then_they_see_that_the_child_has_consent_from_the_parent + and_the_child_should_be_safe_to_vaccinate + end + def given_an_hpv_programme_is_underway - programme = create(:programme, :hpv) + @programme = create(:programme, :hpv) + @organisation = - create(:organisation, :with_one_nurse, programmes: [programme]) + create(:organisation, :with_one_nurse, programmes: [@programme]) @school = create(:school) @@ -35,7 +52,7 @@ def given_an_hpv_programme_is_underway :session, :today, organisation: @organisation, - programmes: [programme], + programmes: [@programme], location: @school ) @@ -62,6 +79,25 @@ def and_there_is_a_child_without_parental_consent expect(page).to have_content(@patient.full_name) end + def and_there_is_a_child_with_gillick_competence + create( + :gillick_assessment, + :competent, + patient_session: @patient.patient_sessions.first, + programme: @programme + ) + end + + def and_the_child_has_a_parent + @parent = create(:parent_relationship, patient: @patient).parent + end + + def when_the_nurse_goes_to_the_child + sign_in @organisation.users.first + + visit session_patient_programme_path(@session, @patient, @programme) + end + def when_the_nurse_assesses_the_child_as_not_being_gillick_competent click_on @patient.full_name click_on "Assess Gillick competence" @@ -176,7 +212,7 @@ def and_the_activity_log_shows_the_gillick_competence click_on "HPV" end - def and_the_child_can_give_their_own_consent_that_the_nurse_records + def and_the_nurse_records_consent_for_the_child click_on "Get consent" # who @@ -197,8 +233,9 @@ def and_the_child_can_give_their_own_consent_that_the_nurse_records choose "Yes, it’s safe to vaccinate" click_on "Continue" + end - # confirmation page + def and_the_child_can_give_their_own_consent_that_the_nurse_records click_on "Change response method" choose "Child (Gillick competent)" 5.times { click_on "Continue" } @@ -208,17 +245,41 @@ def and_the_child_can_give_their_own_consent_that_the_nurse_records expect(page).to have_content("Consent recorded for #{@patient.full_name}") end + def and_changes_the_response_method_to_the_parent + click_on "Change response method" + choose @parent.full_name + click_on "Continue" + + click_on "Continue" + + choose "By phone" + click_on "Continue" + + 3.times { click_on "Continue" } + end + + def then_the_parent_can_give_consent + click_on "Confirm" + end + def when_the_nurse_views_the_childs_record click_on @patient.full_name, match: :first end - def then_they_see_that_the_child_has_consent + def then_they_see_that_the_child_has_consent_from_themselves expect(page).to have_content( "#{@patient.full_name} Child (Gillick competent)" ) expect(page).to have_content("Consent given") end + def then_they_see_that_the_child_has_consent_from_the_parent + expect(page).to have_content( + "Consent given\nNameResponse dateDecision #{@parent.full_name}" + ) + expect(page).to have_content("Consent given") + end + def and_the_child_should_be_safe_to_vaccinate expect(page).to have_content("Safe to vaccinate") end diff --git a/spec/features/triage_partially_vaccinated_spec.rb b/spec/features/triage_partially_vaccinated_spec.rb index 0fd527907d..93ad2c9657 100644 --- a/spec/features/triage_partially_vaccinated_spec.rb +++ b/spec/features/triage_partially_vaccinated_spec.rb @@ -15,8 +15,10 @@ when_i_go_the_session then_i_see_one_patient_needing_consent + and_i_see_no_patients_needing_triage - when_the_parent_gives_consent + when_i_go_the_session + and_the_parent_gives_consent and_i_click_on_triage then_i_see_one_patient_needing_triage and_i_click_on_the_patient @@ -67,7 +69,7 @@ def then_i_see_the_completed_upload end def when_i_go_the_session - click_on "Sessions" + click_on "Sessions", match: :first click_on "Scheduled" click_on @session.location.name end @@ -89,12 +91,21 @@ def then_i_see_one_patient_needing_consent click_on "Update results" expect(page).to have_content("Showing 1 to 1 of 1 children") + end - click_on @session.location.name + def and_i_see_no_patients_needing_triage + click_on "Triage" + + choose "Needs triage" + click_on "Update results" + + expect(page).to have_content("No children matching search criteria found") end - def when_the_parent_gives_consent - Patient.first.consent_status(programme: @programme).given! + def and_the_parent_gives_consent + create(:consent, :given, patient: Patient.first, programme: @programme) + StatusUpdater.call(patient: @patient) + page.refresh end diff --git a/spec/features/triage_spec.rb b/spec/features/triage_spec.rb index da9b671a5b..8e5f6585dc 100644 --- a/spec/features/triage_spec.rb +++ b/spec/features/triage_spec.rb @@ -3,6 +3,13 @@ describe "Triage" do scenario "nurse can triage a patient" do given_a_programme_with_a_running_session + and_a_patient_who_needs_triage_exists + and_a_patient_who_doesnt_need_triage_exists + + when_i_go_to_the_session_triage_tab + then_i_see_the_patient_who_needs_triage + and_i_dont_see_the_patient_who_doesnt_need_triage + when_i_go_to_the_patient_that_needs_triage then_i_see_the_triage_options @@ -36,40 +43,60 @@ def given_a_programme_with_a_running_session organisation: @organisation, vaccine: programmes.first.vaccines.first ) - location = create(:school) - @session = - create(:session, organisation: @organisation, programmes:, location:) - @patient = + + @session = create(:session, organisation: @organisation, programmes:) + end + + def and_a_patient_who_needs_triage_exists + @patient_triage_needed = create( :patient_session, :consent_given_triage_needed, - programmes:, session: @session ).patient + create( :consent, :given, :health_question_notes, :from_granddad, - patient: @patient, - programme: programmes.first + patient: @patient_triage_needed, + programme: @session.programmes.first ) - @patient.reload # Make sure both consents are accessible + @patient_triage_needed.reload # Make sure both consents are accessible end - def when_i_go_to_the_patient_that_needs_triage - sign_in @organisation.users.first + def and_a_patient_who_doesnt_need_triage_exists + @patient_triage_not_needed = + create( + :patient_session, + :consent_given_triage_not_needed, + session: @session + ).patient + end + def when_i_go_to_the_session_triage_tab + sign_in @organisation.users.first visit session_triage_path(@session) + end + + def then_i_see_the_patient_who_needs_triage + expect(page).to have_content(@patient_triage_needed.full_name) + end + + def and_i_dont_see_the_patient_who_doesnt_need_triage + expect(page).not_to have_content(@patient_triage_not_needed.full_name) + end + + def when_i_go_to_the_patient_that_needs_triage choose "Needs triage" click_on "Update results" - - click_link @patient.full_name + click_link @patient_triage_needed.full_name end def when_i_go_to_the_patient - click_link @patient.full_name, match: :first + click_link @patient_triage_needed.full_name, match: :first end def when_i_record_that_they_need_triage @@ -109,19 +136,19 @@ def then_i_see_the_update_triage_link end def and_needs_triage_emails_are_sent_to_both_parents - @patient.parents.each do |parent| + @patient_triage_needed.parents.each do |parent| expect_email_to parent.email, :consent_confirmation_triage, :any end end def and_vaccination_wont_happen_emails_are_sent_to_both_parents - @patient.parents.each do |parent| + @patient_triage_needed.parents.each do |parent| expect_email_to parent.email, :triage_vaccination_wont_happen, :any end end def and_vaccination_will_happen_emails_are_sent_to_both_parents - @patient.parents.each do |parent| + @patient_triage_needed.parents.each do |parent| expect_email_to parent.email, :triage_vaccination_will_happen, :any end end diff --git a/spec/features/user_cis2_authentication_with_empty_role_spec.rb b/spec/features/user_cis2_authentication_with_empty_role_spec.rb index 4e72bef04a..5a881e17e6 100644 --- a/spec/features/user_cis2_authentication_with_empty_role_spec.rb +++ b/spec/features/user_cis2_authentication_with_empty_role_spec.rb @@ -6,21 +6,13 @@ when_i_go_to_the_sessions_page then_i_am_on_the_start_page when_i_click_the_cis2_login_button - then_i_see_the_organisation_not_found_error + then_i_see_the_wrong_workgroup_error end def given_i_am_setup_in_mavis_and_cis2_but_with_an_empty_role @organisation = create :organisation, ods_code: "AB12" - mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", - org_code: @organisation.ods_code, - org_name: @organisation.name, - role_code: "S8002:G8003:R0001", - selected_roleid: nil - ) + mock_cis2_auth(selected_roleid: "") end def when_i_click_the_cis2_login_button @@ -39,9 +31,9 @@ def then_i_see_the_sessions_page expect(page).to have_current_path sessions_path end - def then_i_see_the_organisation_not_found_error + def then_i_see_the_wrong_workgroup_error expect( page - ).to have_heading "You do not have permission to use this service" + ).to have_heading "You’re not in the right workgroup to use this service" end end diff --git a/spec/features/user_cis2_authentication_with_wrong_organisation_spec.rb b/spec/features/user_cis2_authentication_with_wrong_organisation_spec.rb index 4ab69446b1..631773b438 100644 --- a/spec/features/user_cis2_authentication_with_wrong_organisation_spec.rb +++ b/spec/features/user_cis2_authentication_with_wrong_organisation_spec.rb @@ -16,7 +16,7 @@ context "user has no other orgs to select" do scenario "user has wrong organisation selected" do - given_i_am_setup_in_cis2_with_only_one_org + given_i_am_setup_in_cis2_with_only_one_role when_i_go_to_the_start_page then_i_should_see_the_cis2_login_button @@ -31,13 +31,7 @@ def setup_cis2_auth_mock end def given_i_am_setup_in_cis2_but_not_mavis - mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", - org_code: "A9A5A", - org_name: "SAIS Organisation" - ) + mock_cis2_auth(org_code: "A9A5A", org_name: "SAIS Organisation") end def given_my_organisation_has_been_setup_in_mavis @@ -68,14 +62,14 @@ def then_i_see_the_sessions_page expect(page).to have_current_path sessions_path end - def given_i_am_setup_in_cis2_with_only_one_org + def given_i_am_setup_in_cis2_with_only_one_role mock_cis2_auth( uid: "123", given_name: "Nurse", family_name: "Test", org_code: "A9A5A", org_name: "SAIS Organisation", - user_only_has_one_org: true + user_only_has_one_role: true ) end diff --git a/spec/features/user_cis2_authentication_with_wrong_role_spec.rb b/spec/features/user_cis2_authentication_with_wrong_role_spec.rb index 6117502dc1..ac2b4be64b 100644 --- a/spec/features/user_cis2_authentication_with_wrong_role_spec.rb +++ b/spec/features/user_cis2_authentication_with_wrong_role_spec.rb @@ -15,14 +15,7 @@ def given_i_am_setup_in_mavis_and_cis2_but_with_the_wrong_role @organisation = create :organisation, ods_code: "AB12" - mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", - org_code: @organisation.ods_code, - org_name: @organisation.name, - role_code: "S8002:G8003:R0001" - ) + mock_cis2_auth(selected_roleid: "wrong-role") end def when_i_click_the_cis2_login_button @@ -51,9 +44,6 @@ def when_i_click_the_change_role_button_and_select_the_right_role # With don't actually get to select the right role directly in our test # setup so we change the cis2 response to simulate it. mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", org_code: @organisation.ods_code, org_name: @organisation.name, role: :nurse diff --git a/spec/features/user_cis2_authentication_with_wrong_workgroup_spec.rb b/spec/features/user_cis2_authentication_with_wrong_workgroup_spec.rb index e9f0e9f136..e56ad7ed10 100644 --- a/spec/features/user_cis2_authentication_with_wrong_workgroup_spec.rb +++ b/spec/features/user_cis2_authentication_with_wrong_workgroup_spec.rb @@ -15,12 +15,7 @@ def given_i_am_setup_in_mavis_and_cis2_but_with_the_wrong_role @organisation = create :organisation, ods_code: "A9A5A" - mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", - no_workgroup: true - ) + mock_cis2_auth(selected_roleid: "wrong-workgroup") end def when_i_click_the_cis2_login_button @@ -49,13 +44,8 @@ def when_i_click_the_change_role_button_and_select_the_right_role # With don't actually get to select the right role directly in our test # setup so we change the cis2 response to simulate it. mock_cis2_auth( - uid: "123", - given_name: "Nurse", - family_name: "Test", org_code: @organisation.ods_code, - org_name: @organisation.name, - role: :nurse, - workgroups: %w[schoolagedimmunisations] + org_name: @organisation.name ) click_button "Change role" end diff --git a/spec/fixtures/files/onboarding/valid.yaml b/spec/fixtures/files/onboarding/valid.yaml index bd968d4dc7..88b0119301 100644 --- a/spec/fixtures/files/onboarding/valid.yaml +++ b/spec/fixtures/files/onboarding/valid.yaml @@ -2,6 +2,7 @@ organisation: name: NHS Trust email: example@trust.nhs.uk phone: 07700 900815 + phone_instructions: option 1, followed by option 3 ods_code: EXAMPLE careplus_venue_code: EXAMPLE privacy_notice_url: https://example.com/privacy-notice @@ -14,6 +15,7 @@ teams: name: Team 1 email: team-1@trust.nhs.uk phone: 07700 900816 + phone_instructions: option 9 reply_to_id: 24af66c3-d6bd-4b9f-8067-3844f49e08d0 team_2: name: Team 2 diff --git a/spec/fixtures/files/pds/get-patient-response-deceased.json b/spec/fixtures/files/pds/get-patient-response-deceased.json new file mode 100644 index 0000000000..203693435b --- /dev/null +++ b/spec/fixtures/files/pds/get-patient-response-deceased.json @@ -0,0 +1,387 @@ +{ + "resourceType": "Patient", + "id": "9000000009", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "R", + "display": "restricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": ["Jane"], + "family": "Smith", + "prefix": ["Mrs"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "deceasedDateTime": "2010-10-22T00:00:00+00:00", + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + }, + { + "url": "https://fhir.nhs.uk/StructureDefinition/Extension-PDS-RemovalFromRegistration", + "extension": [ + { + "url": "removalFromRegistrationCode", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/PDS-RemovalReasonExitCode", + "code": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2021-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/spec/fixtures/files/pds/get-patient-response.json b/spec/fixtures/files/pds/get-patient-response.json index 203693435b..4f30866b60 100644 --- a/spec/fixtures/files/pds/get-patient-response.json +++ b/spec/fixtures/files/pds/get-patient-response.json @@ -49,7 +49,6 @@ "gender": "female", "birthDate": "2010-10-22", "multipleBirthInteger": 1, - "deceasedDateTime": "2010-10-22T00:00:00+00:00", "generalPractitioner": [ { "id": "254406A3", @@ -103,28 +102,6 @@ } } }, - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", - "extension": [ - { - "url": "deathNotificationStatus", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", - "version": "1.0.0", - "code": "2", - "display": "Formal - death notice received from Registrar of Deaths" - } - ] - } - }, - { - "url": "systemEffectiveDate", - "valueDateTime": "2010-10-22T00:00:00+00:00" - } - ] - }, { "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", "extension": [ diff --git a/spec/forms/batch_form_spec.rb b/spec/forms/batch_form_spec.rb index 6bfaec36bb..353d3af011 100644 --- a/spec/forms/batch_form_spec.rb +++ b/spec/forms/batch_form_spec.rb @@ -4,7 +4,9 @@ subject(:form) { described_class.new } describe "validations" do + it { should validate_length_of(:name).is_at_least(2).is_at_most(100) } it { should validate_presence_of(:name) } + it { should validate_presence_of(:expiry) } it do diff --git a/spec/forms/search_form_spec.rb b/spec/forms/search_form_spec.rb index cc75e33614..d3944626f9 100644 --- a/spec/forms/search_form_spec.rb +++ b/spec/forms/search_form_spec.rb @@ -106,6 +106,54 @@ expect(form.apply(scope, programme:)).to include(patient) end end + + context "searching on name" do + let(:consent_status) { nil } + let(:date_of_birth_day) { nil } + let(:date_of_birth_month) { nil } + let(:date_of_birth_year) { nil } + let(:missing_nhs_number) { nil } + let(:programme_status) { nil } + let(:q) { nil } + let(:register_status) { nil } + let(:session_status) { nil } + let(:triage_status) { nil } + let(:year_groups) { nil } + + let(:patient_a) do + create(:patient, given_name: "Harry", family_name: "Potter") + end + let(:patient_b) do + create(:patient, given_name: "Hari", family_name: "Potter") + end + let(:patient_c) do + create(:patient, given_name: "Arry", family_name: "Pott") + end + let(:patient_d) do + create(:patient, given_name: "Ron", family_name: "Weasley") + end + let(:patient_e) do + create(:patient, given_name: "Ginny", family_name: "Weasley") + end + + context "with no search query" do + let(:q) { nil } + + it "sorts alphabetically by name" do + expect(form.apply(scope)).to eq( + [patient_c, patient_b, patient_a, patient_e, patient_d] + ) + end + end + + context "with some search query" do + let(:q) { "Harry Potter" } + + it "sorts by similarity" do + expect(form.apply(scope)).to eq([patient_a, patient_b, patient_c]) + end + end + end end context "for patient sessions" do diff --git a/spec/helpers/phone_helper_spec.rb b/spec/helpers/phone_helper_spec.rb new file mode 100644 index 0000000000..9120b341c1 --- /dev/null +++ b/spec/helpers/phone_helper_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +describe PhoneHelper do + describe "#format_phone_with_instructions" do + subject(:formatted_phone) { helper.format_phone_with_instructions(entity) } + + context "when phone instructions are present" do + let(:entity) do + create( + :organisation, + name: "Organisation", + email: "organisation@example.com", + phone: "01234 567890", + phone_instructions: "option 1" + ) + end + + it { should eq("01234 567890 (option 1)") } + end + + context "when phone instructions are blank" do + let(:entity) do + create( + :organisation, + name: "Organisation", + email: "organisation@example.com", + phone: "01234 567890", + phone_instructions: nil + ) + end + + it { should eq("01234 567890") } + end + + context "when phone instructions are an empty string" do + let(:entity) do + create( + :organisation, + name: "Organisation", + email: "organisation@example.com", + phone: "01234 567890", + phone_instructions: "" + ) + end + + it { should eq("01234 567890") } + end + end +end diff --git a/spec/jobs/patient_nhs_number_lookup_with_pending_changes_job_spec.rb b/spec/jobs/patient_nhs_number_lookup_with_pending_changes_job_spec.rb new file mode 100644 index 0000000000..201a23902f --- /dev/null +++ b/spec/jobs/patient_nhs_number_lookup_with_pending_changes_job_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +describe PatientNHSNumberLookupWithPendingChangesJob do + subject(:perform_now) { described_class.perform_now(patient) } + + let(:programme) { create(:programme) } + + before { create(:gp_practice, ods_code: "H81109") } + + context "with an NHS number already" do + let(:patient) do + create( + :patient, + nhs_number: nil, + pending_changes: { + nhs_number: "0123456789" + } + ) + end + + it "doesn't change the NHS number" do + expect { perform_now }.not_to change(patient, :pending_changes) + end + end + + shared_examples "an NHS number lookup" do + before do + stub_request( + :get, + "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient" + ).with( + query: { + "_history" => "true", + "address-postalcode" => "SW11 1AA", + "birthdate" => "eq2014-02-18", + "family" => "Smith", + "given" => "John" + } + ).to_return( + body: file_fixture(response_file), + headers: { + "Content-Type" => "application/fhir+json" + } + ) + end + + context "without a match" do + let(:response_file) { "pds/search-patients-no-results-response.json" } + + it "doesn't change the NHS number" do + expect { perform_now }.not_to change(patient, :pending_changes) + end + end + + context "with a match" do + let(:response_file) { "pds/search-patients-response.json" } + + it "sets the NHS number of the patient" do + expect { perform_now }.to change { + patient.reload.with_pending_changes.nhs_number + }.to("9449306168") + end + + it "marks the patient as not invalidated" do + perform_now + expect(patient.reload.with_pending_changes).not_to be_invalidated + end + end + end + + context "with an NHS number already but invalidated" do + let(:organisation) { create(:organisation, programmes: [programme]) } + + let(:patient) do + create( + :patient, + nhs_number: "0123456789", + invalidated_at: Time.current, + organisation:, + pending_changes: { + given_name: "John", + family_name: "Smith", + date_of_birth: Date.new(2014, 2, 18), + address_postcode: "SW11 1AA" + } + ) + end + + it_behaves_like "an NHS number lookup" + end + + context "without an NHS number" do + let(:patient) do + create( + :patient, + nhs_number: nil, + pending_changes: { + given_name: "John", + family_name: "Smith", + date_of_birth: Date.new(2014, 2, 18), + address_postcode: "SW11 1AA" + } + ) + end + + it_behaves_like "an NHS number lookup" + end +end diff --git a/spec/lib/core_ext/string_normalise_whitespace_spec.rb b/spec/lib/core_ext/string_normalise_whitespace_spec.rb new file mode 100644 index 0000000000..e524847252 --- /dev/null +++ b/spec/lib/core_ext/string_normalise_whitespace_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +describe String do + describe "#normalise_whitespace" do + it "removes leading and trailing whitespace" do + expect(" hello ".normalise_whitespace).to eq("hello") + end + + it "replaces multiple spaces with a single space" do + expect("hello world".normalise_whitespace).to eq("hello world") + end + + it "handles tabs and newlines" do + expect("hello\t\nworld".normalise_whitespace).to eq("hello world") + end + + it "returns nil if string is empty after normalization" do + expect(" ".normalise_whitespace).to be_nil + end + + it "returns nil if string is empty" do + expect("".normalise_whitespace).to be_nil + end + + context "with UTF-8 encoded strings" do + it "removes zero-width joiners (ZWJ)" do + string_with_zwj = "1234\u200D567890" + expect(string_with_zwj.normalise_whitespace).to eq("1234567890") + end + + it "converts non-breaking spaces to regular spaces" do + string_with_nbsp = "hello\u00A0world" + expect(string_with_nbsp.normalise_whitespace).to eq("hello world") + end + + it "handles strings with multiple Unicode characters" do + complex_string = " hello\u200D \u00A0 world\u200D " + expect(complex_string.normalise_whitespace).to eq("hello world") + end + end + + context "with non-UTF-8 encoded strings" do + it "does not apply Unicode-specific transformations" do + ascii_string = "hello world".encode(Encoding::ASCII) + expect(ascii_string.normalise_whitespace).to eq("hello world") + end + end + end +end diff --git a/spec/lib/csv_parser_spec.rb b/spec/lib/csv_parser_spec.rb index bfa09cb42b..3a2f7f97f2 100644 --- a/spec/lib/csv_parser_spec.rb +++ b/spec/lib/csv_parser_spec.rb @@ -22,13 +22,13 @@ expect(row[:another_header].header).to eq("Another-Header") end - context "with a non-breaking space" do - let(:data) { "header\u00A0\nvalue\u00A0" } + context "with un-normalised whitespace" do + let(:data) { " header\u200D \u00A0 \n clean \u200D \t value\u00A0 " } it "removes the characters" do row = table.first - expect(row[:header].value).to eq("value") + expect(row[:header].value).to eq("clean value") end end diff --git a/spec/lib/generate/patient_imports_spec.rb b/spec/lib/generate/cohort_imports_spec.rb similarity index 93% rename from spec/lib/generate/patient_imports_spec.rb rename to spec/lib/generate/cohort_imports_spec.rb index 3b35c5647c..7e174dc119 100644 --- a/spec/lib/generate/patient_imports_spec.rb +++ b/spec/lib/generate/cohort_imports_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Generate::PatientImports do +describe Generate::CohortImports do before do organisation = create(:organisation, ods_code: "A9A5A") programme = create(:programme, :hpv) diff --git a/spec/lib/govuk_notify_personalisation_spec.rb b/spec/lib/govuk_notify_personalisation_spec.rb index bc492f15c2..b44194a1fc 100644 --- a/spec/lib/govuk_notify_personalisation_spec.rb +++ b/spec/lib/govuk_notify_personalisation_spec.rb @@ -19,6 +19,7 @@ name: "Organisation", email: "organisation@example.com", phone: "01234 567890", + phone_instructions: "option 1", programmes: ) end @@ -69,6 +70,8 @@ team_email: "organisation@example.com", team_name: "Organisation", team_phone: "01234 567890", + team_phone_instructions_present: "yes", + team_phone_instructions: "option 1", vaccination: "HPV vaccination" } ) diff --git a/spec/lib/reports/careplus_exporter_spec.rb b/spec/lib/reports/careplus_exporter_spec.rb index 9eb7d89ded..04e0d9bf7b 100644 --- a/spec/lib/reports/careplus_exporter_spec.rb +++ b/spec/lib/reports/careplus_exporter_spec.rb @@ -10,9 +10,13 @@ ) end + let(:delivery_method) { :intramuscular } + let(:expected_vaccine_code) do + described_class::PROGRAMME_TYPE_TO_VACCINE_CODE.fetch(programme.type) + end + shared_examples "generates a report" do let(:programmes) { [programme] } - let(:organisation) do create(:organisation, careplus_venue_code: "ABC", programmes:) end @@ -51,6 +55,7 @@ (1..5).each do |i| expect(headers).to include( "Vaccine #{i}", + "Vaccine Code #{i}", "Dose #{i}", "Reason Not Given #{i}", "Site #{i}", @@ -78,6 +83,7 @@ create( :vaccination_record, programme:, + delivery_method:, patient: patient_session.patient, session: patient_session.session, performed_at: 2.weeks.ago @@ -85,6 +91,7 @@ attended_index = headers.index("Attended") vaccine_index = headers.index("Vaccine 1") + vaccine_code_index = headers.index("Vaccine Code 1") batch_index = headers.index("Batch No 1") site_index = headers.index("Site 1") staff_type_index = headers.index("Staff Type") @@ -98,6 +105,7 @@ expect(row[vaccine_index]).to eq( vaccination_record.vaccine.snomed_product_code ) + expect(row[vaccine_code_index]).to eq(expected_vaccine_code) expect(row[batch_index]).to eq(vaccination_record.batch.name) expect(row[site_index]).to eq("ULA") expect(row[staff_type_index]).to eq("IN") @@ -247,6 +255,20 @@ include_examples "generates a report" end + context "FLU programme" do + let(:programme) { create(:programme, :flu) } + + include_examples "generates a report" + end + + context "FLU programme using nasal spray" do + let(:programme) { create(:programme, :flu) } + let(:delivery_method) { :nasal_spray } + let(:expected_vaccine_code) { "FLUENZ" } + + include_examples "generates a report" + end + context "MenACWY programme" do let(:programme) { create(:programme, :menacwy) } diff --git a/spec/lib/update_patients_from_pds_spec.rb b/spec/lib/update_patients_from_pds_spec.rb index 72370db839..bfc55a23f7 100644 --- a/spec/lib/update_patients_from_pds_spec.rb +++ b/spec/lib/update_patients_from_pds_spec.rb @@ -10,8 +10,15 @@ after { Settings.reload! } before do - create_list(:patient, 2) - create_list(:patient, 2, nhs_number: nil) + create_list(:patient, 2, pending_changes: { given_name: "New given name" }) + create_list( + :patient, + 2, + nhs_number: nil, + pending_changes: { + given_name: "New given name" + } + ) end context "when disabled" do @@ -26,27 +33,42 @@ expect { call }.to have_enqueued_job(PatientNHSNumberLookupJob) .on_queue(:default) .exactly(2) - .times + .times.and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob) + .on_queue(:default) + .exactly(4) + .times end it "queues a job for each patient with an NHS number" do expect { call }.to have_enqueued_job(PatientUpdateFromPDSJob) .on_queue(:default) .exactly(2) - .times + .times.and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob) + .on_queue(:default) + .exactly(4) + .times end it "schedules the jobs with a gap between them" do freeze_time do + # stree-ignore expect { call }.to have_enqueued_job(PatientUpdateFromPDSJob).at( Time.current + ).and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob).at( + Time.current + 2.seconds ).and have_enqueued_job(PatientUpdateFromPDSJob).at( - Time.current + 2.seconds - ).and have_enqueued_job(PatientNHSNumberLookupJob).at( - Time.current + 4.seconds - ).and have_enqueued_job(PatientNHSNumberLookupJob).at( - Time.current + 6.seconds - ) + Time.current + 4.seconds + ).and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob).at( + Time.current + 6.seconds + ).and have_enqueued_job(PatientNHSNumberLookupJob).at( + Time.current + 8.seconds + ).and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob).at( + Time.current + 10.seconds + ).and have_enqueued_job(PatientNHSNumberLookupJob).at( + Time.current + 12.seconds + ).and have_enqueued_job(PatientNHSNumberLookupWithPendingChangesJob).at( + Time.current + 14.seconds + ) end end end diff --git a/spec/models/batch_spec.rb b/spec/models/batch_spec.rb index 7ff6215507..6171d9dcd1 100644 --- a/spec/models/batch_spec.rb +++ b/spec/models/batch_spec.rb @@ -49,6 +49,7 @@ it { should be_valid } it { should validate_presence_of(:name) } + it { should validate_length_of(:name).is_at_least(2).is_at_most(100) } it do expect(batch).to validate_uniqueness_of(:expiry).scoped_to( diff --git a/spec/models/class_import_row_spec.rb b/spec/models/class_import_row_spec.rb index fb12fb5dab..6bc4fe77ff 100644 --- a/spec/models/class_import_row_spec.rb +++ b/spec/models/class_import_row_spec.rb @@ -108,6 +108,26 @@ ) end end + + context "vaccination in a session where name-like fields have length greater than 300" do + let(:invalid_name_length) { "a" * 301 } + let(:data) do + { + "CHILD_FIRST_NAME" => invalid_name_length, + "CHILD_LAST_NAME" => invalid_name_length + } + end + + it "has errors" do + expect(class_import_row).to be_invalid + expect(class_import_row.errors["CHILD_FIRST_NAME"]).to include( + "is greater than 300 characters long" + ) + expect(class_import_row.errors["CHILD_LAST_NAME"]).to include( + "is greater than 300 characters long" + ) + end + end end describe "#to_parents" do @@ -192,13 +212,13 @@ family_name: "Smith", gender_code: "male", given_name: "Jimmy", - nhs_number: "0123456789" + nhs_number: "9990000018" ) end it { should eq(existing_patient) } it { should be_male } - it { should have_attributes(nhs_number: "0123456789") } + it { should have_attributes(nhs_number: "9990000018") } it "overwrites registration" do expect(patient.registration).to eq("8AB") @@ -227,6 +247,270 @@ end end end + + context "with an existing patient without gender" do + let(:data) { valid_data.merge("CHILD_GENDER" => "male") } + + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + gender_code: "not_known", + given_name: "Jimmy", + date_of_birth: Date.new(2010, 1, 1) + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming gender" do + expect(patient).to have_attributes(gender_code: "male") + end + + it "doesn't stage the gender differences" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with gender" do + let(:data) { valid_data.merge("CHILD_GENDER" => "male") } + + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + gender_code: "female", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming gender" do + expect(patient).to have_attributes(gender_code: "female") + end + + it "does stage the gender differences" do + expect(patient.pending_changes).to include("gender_code" => "male") + end + end + + context "with an existing patient without preferred names" do + let(:data) do + valid_data.merge( + "CHILD_PREFERRED_FIRST_NAME" => "Jim", + "CHILD_PREFERRED_LAST_NAME" => "Smithy" + ) + end + + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + given_name: "Jimmy", + date_of_birth: Date.new(2010, 1, 1) + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming preferred names" do + expect(patient).to have_attributes( + preferred_given_name: "Jim", + preferred_family_name: "Smithy" + ) + end + + it "doesn't stage the preferred names differences" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with preferred names" do + let(:data) do + valid_data.merge( + "CHILD_PREFERRED_FIRST_NAME" => "Jim", + "CHILD_PREFERRED_LAST_NAME" => "Smithy" + ) + end + + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + given_name: "Jimmy", + preferred_given_name: "Jimothy", + preferred_family_name: "Smithers", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming gender" do + expect(patient).to have_attributes( + preferred_given_name: "Jimothy", + preferred_family_name: "Smithers" + ) + end + + it "does stage the gender differences" do + expect(patient.pending_changes).to include( + "preferred_given_name" => "Jim", + "preferred_family_name" => "Smithy" + ) + end + end + + context "with an existing patient without address" do + let(:data) do + valid_data.merge( + "CHILD_ADDRESS_LINE_1" => "10 Downing Street", + "CHILD_ADDRESS_LINE_2" => "", + "CHILD_TOWN" => "London", + "CHILD_POSTCODE" => "SW1A 1AA" + ) + end + + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + given_name: "Jimmy", + gender_code: "male", + nhs_number: "9990000018", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB", + address_line_1: nil, + address_line_2: nil, + address_town: nil, + address_postcode: "SW1A 1AA" + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming address" do + expect(patient).to have_attributes( + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + address_postcode: "SW1A 1AA" + ) + end + + it "doesn't stage the incoming address" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with an address (with a different postcode)" do + let(:data) do + valid_data.merge( + "CHILD_ADDRESS_LINE_1" => "10 Downing Street", + "CHILD_ADDRESS_LINE_2" => "", + "CHILD_TOWN" => "London", + "CHILD_POSTCODE" => "SW1A 1AA" + ) + end + + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + gender_code: "male", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "20 Woodstock Road", + address_line_2: "", + address_town: "Oxford", + address_postcode: "OX2 6HD", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming address" do + expect(patient).to have_attributes( + address_line_1: "20 Woodstock Road", + address_line_2: "", + address_town: "Oxford", + address_postcode: "OX2 6HD" + ) + end + + it "does stage the address differences" do + expect(patient.pending_changes).to include( + "address_line_1" => "10 Downing Street", + "address_postcode" => "SW1A 1AA", + "address_town" => "London" + ) + end + end + + context "with an existing patient already with an address (with the same postcode)" do + let(:data) do + valid_data.merge( + "CHILD_ADDRESS_LINE_1" => "10 Downing Street", + "CHILD_ADDRESS_LINE_2" => "", + "CHILD_TOWN" => "London", + "CHILD_POSTCODE" => "SW1A 1AA" + ) + end + + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + gender_code: "male", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "20 Woodstock Road", + address_line_2: "", + address_town: "Oxford", + address_postcode: "SW1A 1AA", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does save the incoming address" do + expect(patient).to have_attributes( + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + address_postcode: "SW1A 1AA" + ) + end + + it "doesn't stage the address differences" do + expect(patient.pending_changes).to be_empty + end + end end describe "#to_parent_relationships" do diff --git a/spec/models/cohort_import_row_spec.rb b/spec/models/cohort_import_row_spec.rb index 920bb53831..863054e689 100644 --- a/spec/models/cohort_import_row_spec.rb +++ b/spec/models/cohort_import_row_spec.rb @@ -25,16 +25,17 @@ { "CHILD_ADDRESS_LINE_1" => "10 Downing Street", "CHILD_ADDRESS_LINE_2" => "", - "CHILD_PREFERRED_GIVEN_NAME" => "Jim", - "CHILD_DATE_OF_BIRTH" => "2010-01-01", + "CHILD_TOWN" => "London", + "CHILD_POSTCODE" => "SW1A 1AA", "CHILD_FIRST_NAME" => "Jimmy", - "CHILD_GENDER" => "Male", "CHILD_LAST_NAME" => "Smith", + "CHILD_PREFERRED_FIRST_NAME" => "Jim", + "CHILD_PREFERRED_LAST_NAME" => "Smithy", + "CHILD_DATE_OF_BIRTH" => "2010-01-01", + "CHILD_GENDER" => "Male", "CHILD_NHS_NUMBER" => "9990000018", - "CHILD_POSTCODE" => "SW1A 1AA", "CHILD_REGISTRATION" => "8AB", - "CHILD_SCHOOL_URN" => school_urn, - "CHILD_TOWN" => "London" + "CHILD_SCHOOL_URN" => school_urn } end @@ -232,6 +233,238 @@ expect(patient.pending_changes).to include("registration" => "8AB") end end + + context "with an existing patient without gender" do + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + gender_code: "not_known", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming gender" do + expect(patient).to have_attributes(gender_code: "male") + end + + it "doesn't stage the gender differences" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with gender" do + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + gender_code: "female", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming gender" do + expect(patient).to have_attributes(gender_code: "female") + end + + it "does stage the gender differences" do + expect(patient.pending_changes).to include("gender_code" => "male") + end + end + + context "with an existing patient without preferred names" do + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + family_name: "Smith", + gender_code: "male", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming preferred names" do + expect(patient).to have_attributes( + preferred_given_name: "Jim", + preferred_family_name: "Smithy" + ) + end + + it "doesn't stage the preferred names differences" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with preferred names" do + let!(:existing_patient) do + create( + :patient, + address_postcode: "SW1A 1AA", + given_name: "Jimmy", + family_name: "Smith", + preferred_given_name: "Jimothy", + preferred_family_name: "Smithers", + nhs_number: "9990000018", + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming gender" do + expect(patient).to have_attributes( + preferred_given_name: "Jimothy", + preferred_family_name: "Smithers" + ) + end + + it "does stage the gender differences" do + expect(patient.pending_changes).to include( + "preferred_given_name" => "Jim", + "preferred_family_name" => "Smithy" + ) + end + end + + context "with an existing patient without address (ex. postcode)" do + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + given_name: "Jimmy", + gender_code: "male", + nhs_number: "9990000018", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB", + address_line_1: nil, + address_line_2: nil, + address_town: nil, + address_postcode: "SW1A 1AA" + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming address" do + expect(patient).to have_attributes( + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + address_postcode: "SW1A 1AA" + ) + end + + it "doesn't stage the incoming address" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient with a different address (but matching postcode)" do + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + given_name: "Jimmy", + gender_code: "male", + nhs_number: "9990000018", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB", + address_line_1: "15 Woodstock Road", + address_line_2: "Jericho", + address_town: "Oxford", + address_postcode: "SW1A 1AA" + ) + end + + it { should eq(existing_patient) } + + it "saves the incoming address" do + expect(patient).to have_attributes( + address_line_1: "10 Downing Street", + address_line_2: "", + address_town: "London", + address_postcode: "SW1A 1AA" + ) + end + + it "doesn't stage the incoming address" do + expect(patient.pending_changes).to be_empty + end + end + + context "with an existing patient already with an address" do + let!(:existing_patient) do + create( + :patient, + family_name: "Smith", + gender_code: "male", + given_name: "Jimmy", + nhs_number: "9990000018", + address_line_1: "20 Woodstock Road", + address_line_2: "", + address_town: "Oxford", + address_postcode: "OX2 6HD", + birth_academic_year: 2009, + date_of_birth: Date.new(2010, 1, 1), + registration: "8AB" + ) + end + + it { should eq(existing_patient) } + + it "does not save the incoming address" do + expect(patient).to have_attributes( + address_line_1: "20 Woodstock Road", + address_line_2: "", + address_town: "Oxford", + address_postcode: "OX2 6HD" + ) + end + + it "does stage the address differences" do + expect(patient.pending_changes).to include( + "address_line_1" => "10 Downing Street", + "address_postcode" => "SW1A 1AA", + "address_town" => "London" + ) + end + end end describe "#to_school_move" do diff --git a/spec/models/consent_spec.rb b/spec/models/consent_spec.rb index cd89da6558..901d73db8a 100644 --- a/spec/models/consent_spec.rb +++ b/spec/models/consent_spec.rb @@ -39,6 +39,10 @@ # describe Consent do + describe "validations" do + it { should validate_length_of(:notes).is_at_most(1000) } + end + describe "when consent given by parent or guardian, all health questions are no" do it "does not require triage" do response = build(:consent, :given) diff --git a/spec/models/gillick_assessment_spec.rb b/spec/models/gillick_assessment_spec.rb index f848897173..7424d9f467 100644 --- a/spec/models/gillick_assessment_spec.rb +++ b/spec/models/gillick_assessment_spec.rb @@ -38,7 +38,9 @@ it { should allow_values(true, false).for(:knows_disease) } it { should allow_values(true, false).for(:knows_side_effects) } it { should allow_values(true, false).for(:knows_vaccination) } + it { should_not validate_presence_of(:notes) } + it { should validate_length_of(:notes).is_at_most(1000) } end describe "#gillick_competent?" do diff --git a/spec/models/immunisation_import_row_spec.rb b/spec/models/immunisation_import_row_spec.rb index b3fd40609a..19a1ff94b4 100644 --- a/spec/models/immunisation_import_row_spec.rb +++ b/spec/models/immunisation_import_row_spec.rb @@ -636,6 +636,42 @@ end end + context "vaccination in a session where name-like fields have length greater than 300" do + let(:invalid_name_length) { "a" * 301 } + let(:data) do + { + "SESSION_ID" => "1", + "VACCINATED" => "Y", + "CARE_SETTING" => "2", + "BATCH_NUMBER" => invalid_name_length, + "CLINIC_NAME" => invalid_name_length, + "PERSON_FORENAME" => invalid_name_length, + "PERSON_SURNAME" => invalid_name_length, + "SCHOOL_NAME" => invalid_name_length + } + end + + it "has errors" do + expect(immunisation_import_row).to be_invalid + + expect(immunisation_import_row.errors["BATCH_NUMBER"]).to include( + "is greater than 100 characters long" + ) + expect(immunisation_import_row.errors["CLINIC_NAME"]).to include( + "is greater than 300 characters long" + ) + expect(immunisation_import_row.errors["PERSON_FORENAME"]).to include( + "is greater than 300 characters long" + ) + expect(immunisation_import_row.errors["PERSON_SURNAME"]).to include( + "is greater than 300 characters long" + ) + expect(immunisation_import_row.errors["SCHOOL_NAME"]).to include( + "is greater than 300 characters long" + ) + end + end + context "vaccination in a session without a delivery site" do let(:programmes) { [create(:programme, :flu)] } diff --git a/spec/models/onboarding_spec.rb b/spec/models/onboarding_spec.rb index 0a3d5fc6c4..3a4e5e7187 100644 --- a/spec/models/onboarding_spec.rb +++ b/spec/models/onboarding_spec.rb @@ -27,12 +27,16 @@ expect(organisation.name).to eq("NHS Trust") expect(organisation.email).to eq("example@trust.nhs.uk") expect(organisation.phone).to eq("07700 900815") + expect(organisation.phone_instructions).to eq( + "option 1, followed by option 3" + ) expect(organisation.careplus_venue_code).to eq("EXAMPLE") expect(organisation.programmes).to contain_exactly(programme) team1 = organisation.teams.includes(:schools).find_by!(name: "Team 1") expect(team1.email).to eq("team-1@trust.nhs.uk") expect(team1.phone).to eq("07700 900816") + expect(team1.phone_instructions).to eq("option 9") expect(team1.reply_to_id).to eq("24af66c3-d6bd-4b9f-8067-3844f49e08d0") team2 = organisation.teams.includes(:schools).find_by!(name: "Team 2") diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 58b179682a..1ae1a32cc9 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -13,6 +13,7 @@ # name :text not null # ods_code :string not null # phone :string +# phone_instructions :string # privacy_notice_url :string not null # privacy_policy_url :string not null # created_at :datetime not null diff --git a/spec/models/patient/triage_status_spec.rb b/spec/models/patient/triage_status_spec.rb index ff016b936d..93bb4adc9f 100644 --- a/spec/models/patient/triage_status_spec.rb +++ b/spec/models/patient/triage_status_spec.rb @@ -57,6 +57,28 @@ it { should be(:required) } end + context "with a historical vaccination that needs triage" do + let(:programme) { create(:programme, :td_ipv) } + + before do + create(:vaccination_record, patient:, programme:, dose_sequence: 1) + end + + it { should be(:not_required) } + + context "when consent is given" do + before { create(:consent, :given, patient:, programme:) } + + it { should be(:required) } + end + + context "when consent is refused" do + before { create(:consent, :refused, patient:, programme:) } + + it { should be(:not_required) } + end + end + context "with a safe to vaccinate triage" do before { create(:triage, :ready_to_vaccinate, patient:, programme:) } diff --git a/spec/models/patient_spec.rb b/spec/models/patient_spec.rb index 58da9d4c03..09d9bcb6e0 100644 --- a/spec/models/patient_spec.rb +++ b/spec/models/patient_spec.rb @@ -54,15 +54,13 @@ describe "#search_by_name" do subject(:scope) { described_class.search_by_name(query) } - let(:query) { "Harry Potter" } - let(:patient_a) do # exact match comes first create(:patient, given_name: "Harry", family_name: "Potter") end let(:patient_b) do # similar match comes next - create(:patient, given_name: "Hari", family_name: "Potter") + create(:patient, given_name: "Hari", family_name: "Potte") end let(:patient_c) do # least similar match comes last @@ -73,8 +71,45 @@ create(:patient, given_name: "Ron", family_name: "Weasley") end - it { should eq([patient_a, patient_b, patient_c]) } - it { should_not include(patient_d) } + context "with full name, in `given_name family_name` format" do + let(:query) { "Harry Potter" } + + it "returns the patients in the correct order" do + expect(scope).to eq([patient_a, patient_b, patient_c]) + end + end + + context "with exact name, in `FAMILY_NAME, given_name` format" do + let(:query) { "POTTER, Harry" } + + it "returns the patients in the correct order" do + expect(scope).to eq([patient_a, patient_b, patient_c]) + end + end + + context "with exact name, in `family_name given_name` format" do + let(:query) { "Potter Harry" } + + it "returns the patients in the correct order" do + expect(scope).to eq([patient_a, patient_b, patient_c]) + end + end + + context "with last name only" do + let(:query) { "Potter" } + + it "returns the patients in the correct order" do + expect(scope).to eq([patient_a, patient_b, patient_c]) + end + end + + context "with first name only" do + let(:query) { "Harry" } + + it "returns the patients in the correct order" do + expect(scope).to eq([patient_a]) + end + end end describe "#order_by_name" do diff --git a/spec/models/pds/patient_spec.rb b/spec/models/pds/patient_spec.rb index 7137907dac..56ae774d86 100644 --- a/spec/models/pds/patient_spec.rb +++ b/spec/models/pds/patient_spec.rb @@ -4,7 +4,9 @@ describe "#find" do subject(:find) { described_class.find("9000000009") } - let(:json_response) { file_fixture("pds/get-patient-response.json").read } + let(:json_response) do + file_fixture("pds/get-patient-response-deceased.json").read + end before do allow(NHS::PDS).to receive(:get_patient).and_return( diff --git a/spec/models/pre_screening_spec.rb b/spec/models/pre_screening_spec.rb index f5451951c0..22f1f1ee7c 100644 --- a/spec/models/pre_screening_spec.rb +++ b/spec/models/pre_screening_spec.rb @@ -43,6 +43,8 @@ it { should allow_values(true, false).for(:not_already_had) } it { should allow_values(true, false).for(:not_pregnant) } it { should allow_values(true, false).for(:not_taking_medication) } + it { should_not validate_presence_of(:notes) } + it { should validate_length_of(:notes).is_at_most(1000) } end end diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb index 00192ff902..069778723d 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/team_spec.rb @@ -4,14 +4,15 @@ # # Table name: teams # -# id :bigint not null, primary key -# email :string not null -# name :string not null -# phone :string not null -# created_at :datetime not null -# updated_at :datetime not null -# organisation_id :bigint not null -# reply_to_id :uuid +# id :bigint not null, primary key +# email :string not null +# name :string not null +# phone :string not null +# phone_instructions :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# reply_to_id :uuid # # Indexes # diff --git a/spec/policies/consent_notification_policy_spec.rb b/spec/policies/consent_notification_policy_spec.rb new file mode 100644 index 0000000000..00560d44fd --- /dev/null +++ b/spec/policies/consent_notification_policy_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +describe ConsentNotificationPolicy do + let(:programmes) { [create(:programme, :hpv)] } + let(:organisation) { create(:organisation, programmes: programmes) } + let(:user) do + user = create(:user, organisation: organisation) + allow(user).to receive(:selected_organisation).and_return(organisation) + user + end + + let(:session) { create(:session, organisation:, programmes:) } + let(:consent_notification) do + create(:consent_notification, :request, session:) + end + + let(:other_organisation) { create(:organisation, programmes:) } + let(:other_session) do + create(:session, organisation: other_organisation, programmes:) + end + let(:another_organisations_consent_notification) do + create(:consent_notification, :request, session: other_session) + end + + describe "Scope#resolve" do + subject do + ConsentNotificationPolicy::Scope.new(user, ConsentNotification).resolve + end + + it { should include(consent_notification) } + it { should_not include(another_organisations_consent_notification) } + end +end diff --git a/spec/support/cis2_auth_helper.rb b/spec/support/cis2_auth_helper.rb index 1b7fd21af8..b159b292fc 100644 --- a/spec/support/cis2_auth_helper.rb +++ b/spec/support/cis2_auth_helper.rb @@ -34,30 +34,46 @@ def cis2_auth_info "role_name" => '"Clinical":"Clinical Provision":"Nurse Access Role"', "role_code" => "S8000:G8000:R8001", - "activities" => [ - "Receive Self Claimed LR Alerts", - "Receive Legal Override and Emergency View Alerts", - "Receive Sealing Alerts" - ], - "activity_codes" => %w[B0016 B0015 B0018], + "activities" => [], + "activity_codes" => [], "workgroups" => ["schoolagedimmunisations"], "workgroups_codes" => ["15025792819"] }, + { + "person_orgid" => "1111222233334444", + "person_roleid" => "wrong-role", + "org_code" => "A9A5A", + "role_name" => + '"Clinical":"Clinical Provision":"Health Professional Access Role"', + "role_code" => "S8000:G8000:R8003", + "activities" => [], + "activity_codes" => [], + "workgroups" => ["schoolagedimmunisations"], + "workgroups_codes" => ["15025792819"] + }, + { + "person_orgid" => "1111222233334444", + "person_roleid" => "wrong-workgroup", + "org_code" => "A9A5A", + "role_name" => + '"Clinical":"Clinical Provision":"Nurse Access Role"', + "role_code" => "S8000:G8000:R8001", + "activities" => [], + "activity_codes" => [], + "workgroups" => [], + "workgroups_codes" => [] + }, { "person_orgid" => "1234123412341234", - "person_roleid" => "5678567856785678", + "person_roleid" => "wrong-organisation", "org_code" => "AB12", "role_name" => '"Clinical":"Clinical Provision":"Nurse Access Role"', "role_code" => "S8000:G8000:R8001", - "activities" => [ - "Personal Medication Administration", - "Perform Detailed Health Record", - "Amend Patient Demographics", - "Perform Patient Administration", - "Verify Health Records" - ], - "activity_codes" => %w[B0428 B0380 B0825 B0560 B8028] + "activities" => [], + "activity_codes" => [], + "workgroups" => ["schoolagedimmunisations"], + "workgroups_codes" => ["15025792819"] } ], "given_name" => "Nurse", @@ -106,15 +122,15 @@ def sign_in(user, role: :nurse, org_code: nil, superuser: false) end def mock_cis2_auth( - uid:, - given_name:, - family_name:, + uid: "123", + given_name: "Nurse", + family_name: "Test", email: nil, role: :nurse, role_code: nil, org_code: nil, org_name: "Test SAIS Org", - user_only_has_one_org: false, + user_only_has_one_role: false, workgroups: nil, no_workgroup: false, sid: nil, @@ -128,8 +144,10 @@ def mock_cis2_auth( raw_info["nhsid_user_orgs"][0].merge!(org_code:, org_name:) end - if user_only_has_one_org - raw_info["nhsid_nrbac_roles"].select! { _1["org_code"] == org_code } + if user_only_has_one_role + raw_info["nhsid_nrbac_roles"].select! do + _1["person_roleid"] == selected_roleid + end end role_code ||= { diff --git a/yarn.lock b/yarn.lock index 224b7f9d40..24ada28db8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1225,130 +1225,130 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@esbuild/aix-ppc64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" - integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ== - -"@esbuild/android-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe" - integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ== - -"@esbuild/android-arm@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4" - integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A== - -"@esbuild/android-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009" - integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ== - -"@esbuild/darwin-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b" - integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w== - -"@esbuild/darwin-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a" - integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A== - -"@esbuild/freebsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b" - integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw== - -"@esbuild/freebsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709" - integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q== - -"@esbuild/linux-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30" - integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A== - -"@esbuild/linux-arm@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225" - integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ== - -"@esbuild/linux-ia32@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177" - integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw== - -"@esbuild/linux-loong64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8" - integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g== - -"@esbuild/linux-mips64el@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26" - integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag== - -"@esbuild/linux-ppc64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56" - integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg== - -"@esbuild/linux-riscv64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415" - integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA== - -"@esbuild/linux-s390x@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d" - integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ== - -"@esbuild/linux-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033" - integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA== - -"@esbuild/netbsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259" - integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA== - -"@esbuild/netbsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5" - integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g== - -"@esbuild/openbsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c" - integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ== - -"@esbuild/openbsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2" - integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w== - -"@esbuild/sunos-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f" - integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA== - -"@esbuild/win32-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea" - integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ== - -"@esbuild/win32-ia32@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322" - integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew== - -"@esbuild/win32-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a" - integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg== +"@esbuild/aix-ppc64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162" + integrity sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q== + +"@esbuild/android-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz#d11d4fc299224e729e2190cacadbcc00e7a9fd67" + integrity sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A== + +"@esbuild/android-arm@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz#5660bd25080553dd2a28438f2a401a29959bd9b1" + integrity sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ== + +"@esbuild/android-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz#18ddde705bf984e8cd9efec54e199ac18bc7bee1" + integrity sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ== + +"@esbuild/darwin-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz#b0b7fb55db8fc6f5de5a0207ae986eb9c4766e67" + integrity sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g== + +"@esbuild/darwin-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz#e6813fdeba0bba356cb350a4b80543fbe66bf26f" + integrity sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A== + +"@esbuild/freebsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz#dc11a73d3ccdc308567b908b43c6698e850759be" + integrity sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ== + +"@esbuild/freebsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz#91da08db8bd1bff5f31924c57a81dab26e93a143" + integrity sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ== + +"@esbuild/linux-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz#efc15e45c945a082708f9a9f73bfa8d4db49728a" + integrity sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ== + +"@esbuild/linux-arm@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz#9b93c3e54ac49a2ede6f906e705d5d906f6db9e8" + integrity sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ== + +"@esbuild/linux-ia32@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz#be8ef2c3e1d99fca2d25c416b297d00360623596" + integrity sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ== + +"@esbuild/linux-loong64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz#b0840a2707c3fc02eec288d3f9defa3827cd7a87" + integrity sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA== + +"@esbuild/linux-mips64el@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz#2a198e5a458c9f0e75881a4e63d26ba0cf9df39f" + integrity sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg== + +"@esbuild/linux-ppc64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz#64f4ae0b923d7dd72fb860b9b22edb42007cf8f5" + integrity sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag== + +"@esbuild/linux-riscv64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz#fb2844b11fdddd39e29d291c7cf80f99b0d5158d" + integrity sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA== + +"@esbuild/linux-s390x@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz#1466876e0aa3560c7673e63fdebc8278707bc750" + integrity sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g== + +"@esbuild/linux-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz#c10fde899455db7cba5f11b3bccfa0e41bf4d0cd" + integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA== + +"@esbuild/netbsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz#02e483fbcbe3f18f0b02612a941b77be76c111a4" + integrity sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ== + +"@esbuild/netbsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz#ec401fb0b1ed0ac01d978564c5fc8634ed1dc2ed" + integrity sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw== + +"@esbuild/openbsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz#f272c2f41cfea1d91b93d487a51b5c5ca7a8c8c4" + integrity sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A== + +"@esbuild/openbsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz#2e25950bc10fa9db1e5c868e3d50c44f7c150fd7" + integrity sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw== + +"@esbuild/sunos-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz#cd596fa65a67b3b7adc5ecd52d9f5733832e1abd" + integrity sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q== + +"@esbuild/win32-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz#b4dbcb57b21eeaf8331e424c3999b89d8951dc88" + integrity sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ== + +"@esbuild/win32-ia32@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz#410842e5d66d4ece1757634e297a87635eb82f7a" + integrity sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg== + +"@esbuild/win32-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc" + integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ== "@hotwired/stimulus-webpack-helpers@^1.0.0": version "1.0.1" @@ -2917,36 +2917,36 @@ esbuild-jest@^0.5.0: "@babel/plugin-transform-modules-commonjs" "^7.12.13" babel-jest "^26.6.3" -esbuild@^0.25.3: - version "0.25.3" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285" - integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q== +esbuild@^0.25.4: + version "0.25.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.4.tgz#bb9a16334d4ef2c33c7301a924b8b863351a0854" + integrity sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.3" - "@esbuild/android-arm" "0.25.3" - "@esbuild/android-arm64" "0.25.3" - "@esbuild/android-x64" "0.25.3" - "@esbuild/darwin-arm64" "0.25.3" - "@esbuild/darwin-x64" "0.25.3" - "@esbuild/freebsd-arm64" "0.25.3" - "@esbuild/freebsd-x64" "0.25.3" - "@esbuild/linux-arm" "0.25.3" - "@esbuild/linux-arm64" "0.25.3" - "@esbuild/linux-ia32" "0.25.3" - "@esbuild/linux-loong64" "0.25.3" - "@esbuild/linux-mips64el" "0.25.3" - "@esbuild/linux-ppc64" "0.25.3" - "@esbuild/linux-riscv64" "0.25.3" - "@esbuild/linux-s390x" "0.25.3" - "@esbuild/linux-x64" "0.25.3" - "@esbuild/netbsd-arm64" "0.25.3" - "@esbuild/netbsd-x64" "0.25.3" - "@esbuild/openbsd-arm64" "0.25.3" - "@esbuild/openbsd-x64" "0.25.3" - "@esbuild/sunos-x64" "0.25.3" - "@esbuild/win32-arm64" "0.25.3" - "@esbuild/win32-ia32" "0.25.3" - "@esbuild/win32-x64" "0.25.3" + "@esbuild/aix-ppc64" "0.25.4" + "@esbuild/android-arm" "0.25.4" + "@esbuild/android-arm64" "0.25.4" + "@esbuild/android-x64" "0.25.4" + "@esbuild/darwin-arm64" "0.25.4" + "@esbuild/darwin-x64" "0.25.4" + "@esbuild/freebsd-arm64" "0.25.4" + "@esbuild/freebsd-x64" "0.25.4" + "@esbuild/linux-arm" "0.25.4" + "@esbuild/linux-arm64" "0.25.4" + "@esbuild/linux-ia32" "0.25.4" + "@esbuild/linux-loong64" "0.25.4" + "@esbuild/linux-mips64el" "0.25.4" + "@esbuild/linux-ppc64" "0.25.4" + "@esbuild/linux-riscv64" "0.25.4" + "@esbuild/linux-s390x" "0.25.4" + "@esbuild/linux-x64" "0.25.4" + "@esbuild/netbsd-arm64" "0.25.4" + "@esbuild/netbsd-x64" "0.25.4" + "@esbuild/openbsd-arm64" "0.25.4" + "@esbuild/openbsd-x64" "0.25.4" + "@esbuild/sunos-x64" "0.25.4" + "@esbuild/win32-arm64" "0.25.4" + "@esbuild/win32-ia32" "0.25.4" + "@esbuild/win32-x64" "0.25.4" escalade@^3.1.1: version "3.1.1" @@ -3457,10 +3457,10 @@ idb@^7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== -idb@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.2.tgz#349af3974281879889e0572bbb231f978b9f3cf0" - integrity sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg== +idb@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9" + integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg== immutable@^5.0.2: version "5.0.2" @@ -5174,10 +5174,10 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sass@^1.87.0: - version "1.87.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.87.0.tgz#8cceb36fa63fb48a8d5d7f2f4c13b49c524b723e" - integrity sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw== +sass@^1.88.0: + version "1.88.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.88.0.tgz#cd1495749bebd9e4aca86e93ee60b3904a107789" + integrity sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg== dependencies: chokidar "^4.0.0" immutable "^5.0.2"