Make sure the CSV you upload uses the following columns:
<%= govuk_table(classes: "app-table--csv app-table--small") do |table|
diff --git a/app/components/app_import_format_details_component.rb b/app/components/app_import_format_details_component.rb
index 6d6d90a3b7..33fea34d33 100644
--- a/app/components/app_import_format_details_component.rb
+++ b/app/components/app_import_format_details_component.rb
@@ -10,23 +10,6 @@ def initialize(import:)
delegate :team, to: :@import
delegate :govuk_details, :govuk_table, to: :helpers
- def summary_text
- case @import
- when ClassImport
- "How to format your Mavis CSV file for class lists"
- when CohortImport
- "How to format your Mavis CSV file for child records"
- when ImmunisationImport
- if team.has_national_reporting_access?
- "How to format your CSV file for vaccination records"
- else
- "How to format your Mavis CSV file for vaccination records"
- end
- else
- raise ArgumentError, "Unsupported import type: #{@import.class}"
- end
- end
-
def columns
case @import
when ClassImport
@@ -44,7 +27,7 @@ def class_import_columns
{
name: "CHILD_POSTCODE",
notes:
- "Optional, must be formatted as a valid postcode, for example #{tag.i("SW1A 1AA")}."
+ "Optional, must be a full postcode, for example #{tag.i("SW1A 1AA")}"
}
] + parent_columns
end
@@ -55,7 +38,7 @@ def cohort_import_columns
{
name: "CHILD_POSTCODE",
notes:
- "#{tag.strong("Required")}, must be formatted as a valid postcode, for example #{tag.i("SW1A 1AA")}."
+ "#{tag.strong("Required")}, must be a full postcode, for example #{tag.i("SW1A 1AA")}"
},
{
name: "CHILD_SCHOOL_URN",
@@ -77,33 +60,26 @@ def immunisation_import_columns
def point_of_care_immunisation_import_columns
organisation_code(optionality: "Optional") +
- school_urn(optionality: "Optional") + school_name +
- nhs_number(optionality: "Optional") +
- patient_demographics(optionality: "Required") +
- date_and_time_of_vaccination(date_optionality: "Required") +
+ school_urn(optionality: "Optional") + school_name + nhs_number +
+ patient_demographics + date_and_time_of_vaccination +
vaccinated(
- optionality: "Required",
- extra_notes:
- "Can be omitted if #{tag.code("VACCINE_GIVEN")} is provided."
+ notes:
+ "Enter #{tag.i("Y")} or #{tag.i("N")}. Mavis will assume #{tag.i("Y")} if " \
+ "#{tag.code(" VACCINE_GIVEN ")} is provided."
) + vaccine_and_batch + programme + anatomical_site +
reason_not_vaccinated_and_notes + dose_sequence + care_setting +
performing_professional
end
def national_reporting_immunisation_import_columns
- organisation_code(optionality: "Mandatory") +
- school_urn(optionality: "Mandatory") +
- nhs_number(optionality: "Required") +
- patient_demographics(
- optionality: "Mandatory",
- gender_field_name: "PERSON_GENDER"
- ) +
+ organisation_code(optionality: "Required") +
+ school_urn(optionality: "Required") + nhs_number +
+ patient_demographics(gender_field_name: "PERSON_GENDER") +
vaccinated(
- optionality: "Optional",
- extra_notes:
- "Rows with the value #{tag.i("N")} will not be validated and will not be imported."
- ) + date_and_time_of_vaccination(date_optionality: "Mandatory") +
- national_reporting_vaccine_and_batch +
+ notes:
+ "Optional, enter #{tag.i("Y")} or #{tag.i("N")}. If you enter nothing, Mavis will assume " \
+ "#{tag.i("Y")}. If you enter #{tag.i("N")}, the row will not be uploaded."
+ ) + date_and_time_of_vaccination + national_reporting_vaccine_and_batch +
national_reporting_anatomical_site + national_reporting_dose_sequence +
national_reporting_performing_professional_names + local_patient_id
end
@@ -146,7 +122,7 @@ def child_columns
{
name: "CHILD_YEAR_GROUP",
notes:
- "Optional, numeric, the child’s year group, for example #{tag.i("8")}. If present, and " \
+ "Optional, numeric, for example #{tag.i("8")}. If present, and " \
"when the child’s date of birth would place them in a different year, this value can " \
"be used to override the cohort the child will be placed in."
},
@@ -157,7 +133,7 @@ def child_columns
},
{
name: "CHILD_NHS_NUMBER",
- notes: "Optional, must be 10 digits and numeric."
+ notes: "You must enter a valid NHS number if available."
},
{
name: "CHILD_GENDER",
@@ -181,12 +157,17 @@ def organisation_code(optionality:)
]
end
- def nhs_number(optionality:)
+ def nhs_number
[
{
name: "NHS_NUMBER",
notes:
- "#{tag.strong(optionality)}, must be a valid #{link_to("NHS number", "https://www.datadictionary.nhs.uk/attributes/nhs_number.html")}."
+ "You must enter a valid #{
+ link_to(
+ "NHS number",
+ "https://www.datadictionary.nhs.uk/attributes/nhs_number.html"
+ )
+ } if available."
}
]
end
@@ -212,60 +193,56 @@ def school_name
]
end
- def patient_demographics(
- optionality:,
- gender_field_name: "PERSON_GENDER_CODE"
- )
+ def patient_demographics(gender_field_name: "PERSON_GENDER_CODE")
[
- { name: "PERSON_FORENAME", notes: tag.strong(optionality) },
- { name: "PERSON_SURNAME", notes: tag.strong(optionality) },
+ {
+ name: "PERSON_FORENAME",
+ notes:
+ "#{tag.strong("Required")}, must use alphabetical characters " \
+ "(you can include accents like Chloë, hyphens like Anne-Marie or apostrophes like D'Arcy " \
+ "but no other special characters)."
+ },
+ {
+ name: "PERSON_SURNAME",
+ notes:
+ "#{tag.strong("Required")}, must use alphabetical characters " \
+ "(you can include accents like Jiménez, hyphens like Burne-Jones or apostrophes like O'Hare " \
+ "but no other special characters)."
+ },
{
name: "PERSON_DOB",
notes:
- "#{tag.strong(optionality)}, must use either #{tag.i("YYYYMMDD")} or " \
+ "#{tag.strong("Required")}, you must use either #{tag.i("YYYYMMDD")} or " \
"#{tag.i("DD/MM/YYYY")} format."
},
{
name: gender_field_name,
notes:
- "#{tag.strong(optionality)}, must be one of: #{tag.i("female")}, " \
+ "#{tag.strong("Required")}, must be one of: #{tag.i("female")}, " \
"#{tag.i("male")}, #{tag.i("not known")} or #{tag.i("not specified")}."
},
{
name: "PERSON_POSTCODE",
notes:
- "#{tag.strong(optionality)}, must be formatted as a valid postcode, for example #{tag.i("SW1A 1AA")}."
+ "#{tag.strong("Required")}, must be a full postcode, for example #{tag.i("SW1A 1AA")}"
}
]
end
- def vaccinated(optionality:, extra_notes: "")
- optionality_html =
- if %w[Mandatory Required].include?(optionality)
- tag.strong(optionality)
- else
- optionality
- end
-
- [
- {
- name: "VACCINATED",
- notes:
- "#{optionality_html}, must be #{tag.i("Y")} or #{tag.i("N")}. #{extra_notes}"
- }
- ]
+ def vaccinated(notes:)
+ [{ name: "VACCINATED", notes: }]
end
- def date_and_time_of_vaccination(date_optionality:)
+ def date_and_time_of_vaccination
[
{
name: "DATE_OF_VACCINATION",
notes:
- "#{tag.strong(date_optionality)}, must use either #{tag.i("YYYYMMDD")} or #{tag.i("DD/MM/YYYY")} format."
+ "#{tag.strong("Required")}, you must use either #{tag.i("YYYYMMDD")} or #{tag.i("DD/MM/YYYY")} format."
},
{
name: "TIME_OF_VACCINATION",
- notes: "Optional, must use #{tag.i("HH:MM:SS")} format."
+ notes: "Optional, use #{tag.i("HH:MM:SS")} format."
}
]
end
@@ -300,12 +277,7 @@ def parent_columns
def programme
programmes = team.programmes.flat_map(&:import_names).map { tag.i(it) }
-
- programmes_sentence =
- programmes.to_sentence(
- last_word_connector: " or ",
- two_words_connector: " or "
- )
+ programmes_sentence = to_sentence_with_or(programmes)
[
{
@@ -315,59 +287,50 @@ def programme
]
end
- def make_vaccines_sentence(vaccines:)
- vaccines.to_sentence(
- last_word_connector: " or ",
- two_words_connector: " or "
- )
- end
-
def vaccine_and_batch
vaccines = team.vaccines.pluck(:upload_name).map { tag.i(it) }
- vaccines_sentence = make_vaccines_sentence(vaccines:)
[
{
name: "VACCINE_GIVEN",
- notes: "Optional, must be one of: #{vaccines_sentence}."
- },
- {
- name: "BATCH_NUMBER",
- notes: "Required if #{tag.code("BATCH_EXPIRY_DATE")} is provided."
+ notes: "Optional, must be one of: #{to_sentence_with_or(vaccines)}."
},
+ { name: "BATCH_NUMBER", notes: "Optional" },
{
name: "BATCH_EXPIRY_DATE",
notes:
- "Required if #{tag.code("BATCH_NUMBER")} is provided, must use " \
- "either #{tag.i("YYYYMMDD")} or #{tag.i("DD/MM/YYYY")} format."
+ "Optional, use either #{tag.i("YYYYMMDD")} or #{tag.i("DD/MM/YYYY")} format."
}
]
end
def national_reporting_vaccine_and_batch
- hpv_vaccines =
- Programme.hpv.vaccines.pluck(:nivs_name).compact.map { tag.i(it) }
- flu_vaccines =
- Programme.flu.vaccines.pluck(:nivs_name).compact.map { tag.i(it) }
-
- hpv_vaccines_sentence = make_vaccines_sentence(vaccines: hpv_vaccines)
- flu_vaccines_sentence = make_vaccines_sentence(vaccines: flu_vaccines)
+ vaccine_given_notes_per_programme =
+ [Programme.hpv, Programme.flu].map do |programme|
+ vaccines =
+ programme
+ .vaccines
+ .where.not(nivs_name: [nil, ""])
+ .order(:nivs_name)
+ .pluck(:nivs_name)
+ .map { tag.i(it) }
+
+ "#{tag.br}#{tag.br}" \
+ "For #{programme.name_in_sentence} records, must be one of: " \
+ "#{to_sentence_with_or(vaccines)}."
+ end
[
{
name: "VACCINE_GIVEN",
notes:
- "#{tag.strong("Mandatory")}" \
- "#{tag.br}#{tag.br}" \
- "For HPV records, must be one of: #{hpv_vaccines_sentence}." \
- "#{tag.br}#{tag.br}" \
- "For flu records, must be one of: #{flu_vaccines_sentence}."
+ ([tag.strong("Required")] + vaccine_given_notes_per_programme).join
},
- { name: "BATCH_NUMBER", notes: tag.strong("Mandatory") },
+ { name: "BATCH_NUMBER", notes: tag.strong("Required") },
{
name: "BATCH_EXPIRY_DATE",
notes:
- "#{tag.strong("Mandatory")}, must use #{tag.i("YYYYMMDD")} format."
+ "#{tag.strong("Required")}, must use #{tag.i("YYYYMMDD")} format."
}
]
end
@@ -394,7 +357,7 @@ def anatomical_site
def reason_not_vaccinated_and_notes
reasons =
ImmunisationImportRow::REASONS_NOT_ADMINISTERED.keys.sort.map do
- tag.i(_1)
+ tag.i(it)
end
reasons_sentence =
reasons.to_sentence(
@@ -466,11 +429,11 @@ def national_reporting_performing_professional_names
[
{
name: "PERFORMING_PROFESSIONAL_FORENAME",
- notes: "Mandatory for flu records, optional for HPV records."
+ notes: "Required for flu records, optional for HPV records."
},
{
name: "PERFORMING_PROFESSIONAL_SURNAME",
- notes: "Mandatory for flu records, optional for HPV records."
+ notes: "Required for flu records, optional for HPV records."
}
]
end
@@ -485,7 +448,7 @@ def supplier
end
def national_reporting_anatomical_site
- sites = ImmunisationImportRow::DELIVERY_SITES.keys.sort.map { tag.i(_1) }
+ sites = ImmunisationImportRow::DELIVERY_SITES.keys.sort.map { tag.i(it) }
site_sentence =
sites.to_sentence(
@@ -496,7 +459,7 @@ def national_reporting_anatomical_site
[
{
name: "ANATOMICAL_SITE",
- notes: "#{tag.strong("Mandatory")}, must be one of: #{site_sentence}."
+ notes: "#{tag.strong("Required")}, must be one of: #{site_sentence}."
}
]
end
@@ -509,7 +472,7 @@ def national_reporting_dose_sequence
{
name: "DOSE_SEQUENCE",
notes:
- "Mandatory for HPV records, optional for flu records." \
+ "Required for HPV records, optional for flu records." \
"#{tag.br} #{tag.br}" \
"Must be a number between 1 and #{hpv_max} for HPV records and between 1 and #{flu_max} for flu records."
}
@@ -518,8 +481,20 @@ def national_reporting_dose_sequence
def local_patient_id
[
- { name: "LOCAL_PATIENT_ID", notes: tag.strong("Mandatory") },
- { name: "LOCAL_PATIENT_ID_URI", notes: tag.strong("Mandatory") }
+ {
+ name: "LOCAL_PATIENT_ID",
+ notes:
+ "#{tag.strong("Required")}, supplied automatically by your vaccination recording system."
+ },
+ {
+ name: "LOCAL_PATIENT_ID_URI",
+ notes:
+ "#{tag.strong("Required")}, supplied automatically by your vaccination recording system."
+ }
]
end
+
+ def to_sentence_with_or(items)
+ items.to_sentence(last_word_connector: " or ", two_words_connector: " or ")
+ end
end
diff --git a/app/components/app_parent_summary_component.rb b/app/components/app_parent_summary_component.rb
deleted file mode 100644
index c5d2ea146c..0000000000
--- a/app/components/app_parent_summary_component.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-# frozen_string_literal: true
-
-class AppParentSummaryComponent < ViewComponent::Base
- def initialize(parent_relationship:, change_links: {})
- @parent_relationship = parent_relationship
- @parent = parent_relationship.parent
- @patient = parent_relationship.patient
-
- @change_links = change_links
- end
-
- def call
- govuk_summary_list do |summary_list|
- summary_list.with_row do |row|
- row.with_key { "Name" }
-
- if @parent.full_name.present?
- row.with_value { @parent.full_name }
- if (href = @change_links[:name])
- row.with_action(text: "Change", href:, visually_hidden_text: "name")
- end
- elsif (href = @change_links[:name])
- row.with_value { link_to("Add name", href) }
- else
- row.with_value { "Not provided" }
- end
- end
-
- summary_list.with_row do |row|
- row.with_key { "Relationship" }
- row.with_value { @parent_relationship.label }
- if (href = @change_links[:relationship])
- row.with_action(
- text: "Change",
- href:,
- visually_hidden_text: "relationship"
- )
- end
- end
-
- unless @patient&.restricted?
- summary_list.with_row do |row|
- row.with_key { "Email address" }
- if @parent.email.present?
- row.with_value { email_address }
- if (href = @change_links[:email])
- row.with_action(
- text: "Change",
- href:,
- visually_hidden_text: "email address"
- )
- end
- elsif (href = @change_links[:email])
- row.with_value { link_to("Add email address", href) }
- else
- row.with_value { "Not provided" }
- end
- end
-
- summary_list.with_row do |row|
- row.with_key { "Phone number" }
- if @parent.phone.present?
- row.with_value { phone_number }
- if (href = @change_links[:phone])
- row.with_action(
- text: "Change",
- href:,
- visually_hidden_text: "phone number"
- )
- end
- elsif (href = @change_links[:phone])
- row.with_value { link_to("Add phone number", href) }
- else
- row.with_value { "Not provided" }
- end
- end
-
- if @parent.contact_method_type.present?
- summary_list.with_row do |row|
- row.with_key { "Communication needs" }
- row.with_value { @parent.contact_method_description }
- if (href = @change_links[:phone])
- row.with_action(
- text: "Change",
- href:,
- visually_hidden_text: "communication needs"
- )
- end
- end
- end
-
- summary_list.with_row do |row|
- row.with_key { "Get updates by text message" }
- row.with_value { @parent.phone_receive_updates ? "Yes" : "No" }
- if (href = @change_links[:phone])
- row.with_action(
- text: "Change",
- href:,
- visually_hidden_text: "get updates by text message"
- )
- end
- end
- end
- end
- end
-
- private
-
- delegate :govuk_summary_list, to: :helpers
-
- EMAIL_FAILURE_TEXTS = {
- "permanent_failure" => "Email address does not exist",
- "temporary_failure" => "Inbox not accepting messages right now"
- }.freeze
-
- def email_address
- delivery_status = @parent.email_delivery_status
-
- elements = [
- tag.p(@parent.email, class: "nhsuk-u-margin-0"),
- if (failure_text = EMAIL_FAILURE_TEXTS[delivery_status])
- render AppStatusComponent.new(
- text: failure_text,
- colour: "red",
- small: true
- )
- end
- ].compact
-
- safe_join(elements)
- end
-
- PHONE_FAILURE_TEXTS = {
- "not_uk_mobile_number_failure" =>
- "Phone number is a landline not accepting text messages",
- "permanent_failure" => "Phone number does not exist",
- "temporary_failure" => "Inbox not accepting messages right now"
- }.freeze
-
- def phone_number
- delivery_status = @parent.sms_delivery_status
-
- elements = [
- tag.p(@parent.phone, class: "nhsuk-u-margin-0"),
- if (failure_text = PHONE_FAILURE_TEXTS[delivery_status])
- render AppStatusComponent.new(
- text: failure_text,
- colour: "red",
- small: true
- )
- end
- ].compact
-
- safe_join(elements)
- end
-end
diff --git a/app/components/app_session_search_form_component.rb b/app/components/app_session_search_form_component.rb
index b9ebc11e1d..25aa42cdbd 100644
--- a/app/components/app_session_search_form_component.rb
+++ b/app/components/app_session_search_form_component.rb
@@ -13,7 +13,7 @@ def initialize(form, url:, programmes:, academic_years:)
STATUSES = %w[in_progress unscheduled scheduled completed].freeze
TYPES = {
- "school" => "School session",
+ "gias_school" => "School session",
"generic_clinic" => "Community clinic"
}.freeze
diff --git a/app/components/app_session_summary_component.rb b/app/components/app_session_summary_component.rb
index 27f5773a20..2c8a934442 100644
--- a/app/components/app_session_summary_component.rb
+++ b/app/components/app_session_summary_component.rb
@@ -63,7 +63,7 @@ def location_row
end
def school_urn_row
- return unless show_location && location.school?
+ return unless show_location && location.gias_school?
{
key: {
diff --git a/app/components/app_timeline_component.rb b/app/components/app_timeline_component.rb
index 6068ece5b7..9aa020fa13 100644
--- a/app/components/app_timeline_component.rb
+++ b/app/components/app_timeline_component.rb
@@ -38,7 +38,7 @@ def render? = @items.present?
"Triage" => "red",
"VaccinationRecord" => "grey",
"SchoolMove" => "orange",
- "SchoolMoveLogEntry" => "pink"
+ "SchoolMoveLogEntry" => "orange"
}.freeze
def format_heading(item)
@@ -63,6 +63,8 @@ def format_description(item)
"#{id_info}#{formatted_details}"
elsif item[:description].present?
item[:description]
+ else
+ ""
end
end
diff --git a/app/controllers/api/reporting/totals_controller.rb b/app/controllers/api/reporting/totals_controller.rb
index db376554c8..a90c840a56 100644
--- a/app/controllers/api/reporting/totals_controller.rb
+++ b/app/controllers/api/reporting/totals_controller.rb
@@ -25,6 +25,13 @@ class API::Reporting::TotalsController < API::Reporting::BaseController
patient_school_name: "School Name"
}.freeze
+ SCHOOL_LOCATION_TYPES = %w[gias_school generic_school].freeze
+ COMMUNITY_LOCATION_TYPES = %w[
+ generic_clinic
+ community_clinic
+ gp_practice
+ ].freeze
+
METRIC_HEADERS = {
cohort: "Cohort",
no_consent: "No Consent",
@@ -179,7 +186,7 @@ def apply_year_group_filter
def team_vaccination_records_scope
VaccinationRecord
.where(discarded_at: nil, outcome: :administered)
- .joins(session: :team_location)
+ .joins(session: { team_location: :location })
.where(
team_locations: {
team_id: @team&.id || current_user.team_ids,
@@ -205,24 +212,50 @@ def team_vaccination_records_scope
end
def team_vaccinations_given_count
- team_vaccination_records_scope.count
+ pivot_location_counts(
+ team_vaccination_records_scope.group("locations.type").count
+ )
end
def team_monthly_vaccinations_given
- counts =
- team_vaccination_records_scope
- .group(
- Arel.sql(
- "EXTRACT(YEAR FROM vaccination_records.performed_at_date)::integer"
- ),
- Arel.sql(
- "EXTRACT(MONTH FROM vaccination_records.performed_at_date)::integer"
- )
- )
- .count
- .map do |(year, month), count|
- { year:, month: Date::MONTHNAMES[month], count: }
- end
- counts.sort_by! { [it[:year], Date::MONTHNAMES.index(it[:month])] }
+ counts_by_key =
+ team_vaccination_records_scope.group(
+ Arel.sql(
+ "EXTRACT(YEAR FROM vaccination_records.performed_at_date)::integer"
+ ),
+ Arel.sql(
+ "EXTRACT(MONTH FROM vaccination_records.performed_at_date)::integer"
+ ),
+ "locations.type"
+ ).count
+
+ grouped = Hash.new { |h, k| h[k] = { school_count: 0, community_count: 0 } }
+
+ counts_by_key.each do |(year, month, loc_type), count|
+ key = [year, month]
+ if SCHOOL_LOCATION_TYPES.include?(loc_type)
+ grouped[key][:school_count] += count
+ elsif COMMUNITY_LOCATION_TYPES.include?(loc_type)
+ grouped[key][:community_count] += count
+ end
+ end
+
+ result =
+ grouped.map do |(year, month), counts|
+ { year:, month: Date::MONTHNAMES[month], **counts }
+ end
+ result.sort_by! { [it[:year], Date::MONTHNAMES.index(it[:month])] }
+ end
+
+ def pivot_location_counts(counts_by_type)
+ school_count =
+ counts_by_type.sum do |type, count|
+ SCHOOL_LOCATION_TYPES.include?(type) ? count : 0
+ end
+ community_count =
+ counts_by_type.sum do |type, count|
+ COMMUNITY_LOCATION_TYPES.include?(type) ? count : 0
+ end
+ { school_count:, community_count: }
end
end
diff --git a/app/controllers/class_imports_controller.rb b/app/controllers/class_imports_controller.rb
index 6f319f966e..8b4a48bf94 100644
--- a/app/controllers/class_imports_controller.rb
+++ b/app/controllers/class_imports_controller.rb
@@ -5,7 +5,7 @@ class ClassImportsController < ApplicationController
before_action :set_draft_import, only: %i[new create]
before_action :set_class_import,
- only: %i[show update approve cancel re_review imported_records]
+ only: %i[show approve cancel re_review imported_records]
before_action :set_open_sections, only: %i[show]
before_action :set_review_records, only: %i[re_review imported_records]
@@ -84,12 +84,6 @@ def show
}
end
- def update
- @class_import.process!
-
- redirect_to class_import_path(@class_import)
- end
-
def re_review
render template: "imports/re_review",
layout: "full",
diff --git a/app/controllers/cohort_imports_controller.rb b/app/controllers/cohort_imports_controller.rb
index 935e43a93a..7d1a08239c 100644
--- a/app/controllers/cohort_imports_controller.rb
+++ b/app/controllers/cohort_imports_controller.rb
@@ -5,7 +5,7 @@ class CohortImportsController < ApplicationController
before_action :set_draft_import, only: %i[new create]
before_action :set_cohort_import,
- only: %i[show update approve cancel re_review imported_records]
+ only: %i[show approve cancel re_review imported_records]
before_action :set_open_sections, only: %i[show]
before_action :set_review_records, only: %i[re_review imported_records]
@@ -89,12 +89,6 @@ def show
}
end
- def update
- @cohort_import.process!
-
- redirect_to cohort_import_path(@cohort_import)
- end
-
def re_review
render template: "imports/re_review",
layout: "full",
diff --git a/app/controllers/draft_imports_controller.rb b/app/controllers/draft_imports_controller.rb
index 5d96dfb050..720ece00e8 100644
--- a/app/controllers/draft_imports_controller.rb
+++ b/app/controllers/draft_imports_controller.rb
@@ -38,7 +38,7 @@ def set_location
end
def set_location_options
- @location_options = policy_scope(Location).school
+ @location_options = policy_scope(Location).gias_school
end
def set_year_group_options
diff --git a/app/controllers/draft_schools_controller.rb b/app/controllers/draft_schools_controller.rb
index 77bfe01c1e..f46e9bcf64 100644
--- a/app/controllers/draft_schools_controller.rb
+++ b/app/controllers/draft_schools_controller.rb
@@ -67,7 +67,7 @@ def set_school
def set_school_options
@school_options =
policy_scope(Location)
- .school
+ .gias_school
.joins(:team_locations)
.where(team_locations: { academic_year: AcademicYear.pending })
.distinct
diff --git a/app/controllers/draft_sessions_controller.rb b/app/controllers/draft_sessions_controller.rb
index 6b133b7ea0..9645a3522f 100644
--- a/app/controllers/draft_sessions_controller.rb
+++ b/app/controllers/draft_sessions_controller.rb
@@ -69,7 +69,7 @@ def set_steps
def set_schools
@schools =
policy_scope(Location)
- .school
+ .gias_school
.joins(:team_locations)
.where(
team_locations: {
diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb
index 6724f84ddb..43b11b58dd 100644
--- a/app/controllers/draft_vaccination_records_controller.rb
+++ b/app/controllers/draft_vaccination_records_controller.rb
@@ -307,7 +307,7 @@ def set_batches
def set_locations
if @draft_vaccination_record.national_reporting_user_and_record?
@location_query = params[:q]
- scope = Location.school.where(status: "open")
+ scope = Location.gias_school.where(status: "open")
if @location_query.present?
scope = scope.search_by_name(@location_query)
diff --git a/app/controllers/immunisation_imports_controller.rb b/app/controllers/immunisation_imports_controller.rb
index 1de2b78f94..a07b384266 100644
--- a/app/controllers/immunisation_imports_controller.rb
+++ b/app/controllers/immunisation_imports_controller.rb
@@ -3,7 +3,7 @@
class ImmunisationImportsController < ApplicationController
include Pagy::Backend
- before_action :set_immunisation_import, only: %i[show update]
+ before_action :set_immunisation_import, only: %i[show]
skip_after_action :verify_policy_scoped, only: %i[new create]
@@ -56,12 +56,6 @@ def show
}
end
- def update
- @immunisation_import.process!
-
- redirect_to immunisation_import_path(@immunisation_import)
- end
-
private
def type
diff --git a/app/controllers/inspect/graphs_controller.rb b/app/controllers/inspect/graphs_controller.rb
index 6f04691821..26e9b5803f 100644
--- a/app/controllers/inspect/graphs_controller.rb
+++ b/app/controllers/inspect/graphs_controller.rb
@@ -14,104 +14,147 @@ class GraphsController < ApplicationController
def show
authorize :inspect, :graph?
- @primary_type = safe_get_primary_type
- if @primary_type.nil?
+ if primary_type.nil?
render plain:
"You don't have permission to view object type: #{params[:object_type].to_s.downcase.singularize}",
status: :bad_request and return
end
- @primary_id = params[:object_id]
# Set default relationships when loading a page for the first time
if params[:relationships].blank? &&
- GraphRecords::DEFAULT_TRAVERSALS.key?(@primary_type)
- default_rels = GraphRecords::DEFAULT_TRAVERSALS[@primary_type] || {}
+ GraphRecords::DEFAULT_TRAVERSALS.key?(primary_type)
+ default_rels = GraphRecords::DEFAULT_TRAVERSALS[primary_type] || {}
new_params = params.to_unsafe_h.merge("relationships" => default_rels)
redirect_to inspect_path(new_params) and return
end
- @object = @primary_type.to_s.classify.constantize.find(@primary_id)
-
- @traversals_config = build_traversals_config
- @graph_params = build_graph_params
- set_pii_settings
-
@graph_record =
GraphRecords.new(
- traversals_config: @traversals_config,
- primary_type: @primary_type,
+ traversals_config:,
+ primary_type:,
clickable: true,
- show_pii: @show_pii
+ show_pii:
)
- @mermaid = @graph_record.graph(**@graph_params).join("\n")
+ @mermaid = @graph_record.graph(**graph_params).join("\n")
end
private
- def set_pii_settings
- @user_is_allowed_to_access_pii = policy(:inspect).show_pii?
+ def show_pii
+ return @show_pii if defined?(@show_pii)
@show_pii =
- @user_is_allowed_to_access_pii &&
- (params[:show_pii] || SHOW_PII_BY_DEFAULT)
+ user_is_allowed_to_access_pii && pii_access_requested_by_user &&
+ !sensitive_patient_in_graph
end
- def build_traversals_config
- traversals_config = {}
- to_process = Set.new([@primary_type])
- processed = Set.new
+ def user_is_allowed_to_access_pii
+ if defined?(@user_is_allowed_to_access_pii)
+ return @user_is_allowed_to_access_pii
+ end
+ @user_is_allowed_to_access_pii = policy(:inspect).show_pii?
+ end
- # Process types until we've explored all connected relationships
- while (type = to_process.first)
- to_process.delete(type)
- processed.add(type)
+ def sensitive_patient_in_graph
+ if defined?(@sensitive_patient_in_graph)
+ return @sensitive_patient_in_graph
+ end
+ @sensitive_patient_in_graph =
+ begin
+ graph_with_pii =
+ GraphRecords.new(
+ traversals_config:,
+ primary_type:,
+ clickable: true,
+ show_pii: true
+ )
+ graph_with_pii.graph(**graph_params)
+ graph_with_pii.patients_with_pii_in_graph.any?(&:restricted?)
+ end
+ end
- selected_rels =
- Array(params.dig(:relationships, type)).reject(&:blank?).map(&:to_sym)
+ def pii_access_requested_by_user
+ if defined?(@pii_access_requested_by_user)
+ return @pii_access_requested_by_user
+ end
+ @pii_access_requested_by_user = params[:show_pii] || SHOW_PII_BY_DEFAULT
+ end
- traversals_config[type] = selected_rels
+ def traversals_config
+ @traversals_config ||=
+ begin
+ traversals = {}
+ to_process = Set.new([primary_type])
+ processed = Set.new
+
+ # Process types until we've explored all connected relationships
+ while (type = to_process.first)
+ to_process.delete(type)
+ processed.add(type)
+
+ selected_rels =
+ Array(params.dig(:relationships, type)).reject(&:blank?).map(
+ &:to_sym
+ )
- # Add target types to process queue
- klass = type.to_s.classify.constantize
- selected_rels.each do |rel|
- association = klass.reflect_on_association(rel)
- next unless association
+ traversals[type] = selected_rels
- target_type = association.klass.name.underscore.to_sym
- to_process.add(target_type) unless processed.include?(target_type)
- end
- end
+ # Add target types to process queue
+ klass = type.to_s.classify.constantize
+ selected_rels.each do |rel|
+ association = klass.reflect_on_association(rel)
+ next unless association
+
+ target_type = association.klass.name.underscore.to_sym
+ to_process.add(target_type) unless processed.include?(target_type)
+ end
+ end
- traversals_config
+ traversals
+ end
end
- def build_graph_params
- graph_params = { @primary_type => [@object.id] }
-
- if params[:additional_ids].present?
- params[:additional_ids].each do |type, ids_string|
- next if ids_string.blank?
- additional_ids = ids_string.split(",").map { |s| s.strip.to_i }
- next unless additional_ids.any?
- type_sym = type.to_sym
- graph_params[type_sym] ||= []
- graph_params[type_sym].concat(additional_ids)
+ def graph_params
+ @graph_params ||=
+ begin
+ graph_params = { primary_type => [primary_object.id] }
+
+ if params[:additional_ids].present?
+ params[:additional_ids].each do |type, ids_string|
+ next if ids_string.blank?
+ additional_ids = ids_string.split(",").map { |s| s.strip.to_i }
+ next unless additional_ids.any?
+ type_sym = type.to_sym
+ graph_params[type_sym] ||= []
+ graph_params[type_sym].concat(additional_ids)
+ end
+ end
+
+ graph_params
end
- end
+ end
- graph_params
+ def primary_type
+ @primary_type ||=
+ begin
+ singular_type = params[:object_type].downcase.singularize
+ return nil unless GraphRecords::ALLOWED_TYPES.include?(singular_type)
+ singular_type.to_sym
+ end
end
- def safe_get_primary_type
- singular_type = params[:object_type].downcase.singularize
- return nil unless GraphRecords::ALLOWED_TYPES.include?(singular_type)
- singular_type.to_sym
+ def primary_object
+ @primary_object ||=
+ begin
+ @primary_id = params[:object_id]
+ primary_type.to_s.classify.constantize.find(@primary_id)
+ end
end
def pii_accessed?
- return false unless @show_pii
+ return false unless show_pii
- build_traversals_config.any? do |from_type, rels|
+ traversals_config.any? do |from_type, rels|
GraphRecords::EXTRA_DETAIL_WHITELIST_WITH_PII.key?(
from_type.name.underscore.to_sym
) ||
@@ -142,7 +185,7 @@ def record_access_log_entry
controller: "graph",
action: "show_pii",
request_details: {
- primary_type: @primary_type,
+ primary_type:,
primary_id: @primary_id,
additional_ids: additional_ids.presence,
visible_fields: request_details
diff --git a/app/controllers/inspect/timeline/patients_controller.rb b/app/controllers/inspect/timeline/patients_controller.rb
index 1b98af6ae5..c7b2e86451 100644
--- a/app/controllers/inspect/timeline/patients_controller.rb
+++ b/app/controllers/inspect/timeline/patients_controller.rb
@@ -13,12 +13,9 @@ class Inspect::Timeline::PatientsController < ApplicationController
def show
authorize :inspect, :timeline?
- set_pii_settings
params[:audit_config] ||= {}
- compare_option = params[:compare_option] || nil
-
# Set default values if none present
if params[:detail_config].nil? && params[:event_names].nil? &&
params[:show_pii].nil?
@@ -40,7 +37,6 @@ def show
params[:audit_config][:include_filtered_audit_changes]
}
- @compare_patient = sample_patient(params[:compare_option]) if compare_option
@event_names = params[:event_names] || []
record_access_log_entry
@@ -48,9 +44,9 @@ def show
@patient_timeline =
TimelineRecords.new(
@patient,
- detail_config: build_details_config,
- audit_config: audit_config,
- show_pii: @show_pii
+ details_config:,
+ audit_config:,
+ show_pii:
).load_timeline_events(@event_names)
@no_events_message = true if @patient_timeline.empty?
@@ -61,9 +57,9 @@ def show
@compare_patient_timeline =
TimelineRecords.new(
@compare_patient,
- detail_config: build_details_config,
- audit_config: audit_config,
- show_pii: @show_pii
+ details_config:,
+ audit_config:,
+ show_pii:
).load_timeline_events(@event_names)
@no_events_compare_message = true if @compare_patient_timeline.empty?
@@ -72,11 +68,33 @@ def show
private
- def set_pii_settings
- @user_is_allowed_to_access_pii = policy(:inspect).show_pii?
+ def show_pii
+ return @show_pii if defined?(@show_pii)
@show_pii =
- @user_is_allowed_to_access_pii &&
- (params[:show_pii] || SHOW_PII_BY_DEFAULT)
+ user_is_allowed_to_access_pii && pii_access_requested_by_user &&
+ !patient_accessed_is_sensitive
+ end
+
+ def user_is_allowed_to_access_pii
+ if defined?(@user_is_allowed_to_access_pii)
+ return @user_is_allowed_to_access_pii
+ end
+ @user_is_allowed_to_access_pii = policy(:inspect).show_pii?
+ end
+
+ def patient_accessed_is_sensitive
+ if defined?(@patient_accessed_is_sensitive)
+ return @patient_accessed_is_sensitive
+ end
+ @patient_accessed_is_sensitive =
+ @patient.restricted? || @compare_patient&.restricted?
+ end
+
+ def pii_access_requested_by_user
+ if defined?(@pii_access_requested_by_user)
+ return @pii_access_requested_by_user
+ end
+ @pii_access_requested_by_user = params[:show_pii] || SHOW_PII_BY_DEFAULT
end
def set_patient
@@ -84,6 +102,9 @@ def set_patient
timeline = TimelineRecords.new(@patient)
@patient_events = timeline.patient_events(@patient)
@additional_events = timeline.additional_events(@patient)
+
+ compare_option = params[:compare_option] || nil
+ @compare_patient = sample_patient(params[:compare_option]) if compare_option
end
# TODO: Fix so that a new comparison patient isn't sampled every time
@@ -118,22 +139,42 @@ def sample_patient(compare_option)
end
end
- def build_details_config
- details_params = params[:detail_config] || {}
- details_params = details_params.to_unsafe_h unless details_params.is_a?(
- Hash
- )
+ def details_config
+ @details_config ||=
+ begin
+ details_params = params[:detail_config] || {}
+ details_params = details_params.to_unsafe_h unless details_params.is_a?(
+ Hash
+ )
- details_params.each_with_object({}) do |(event_type, fields), hash|
- selected_fields = Array(fields).reject(&:blank?).map(&:to_sym)
- hash[event_type.to_sym] = selected_fields
- end
+ event_list_details =
+ (@event_names - ["audits"]).map { [it.to_sym, []] }.to_h
+ user_submitted_details =
+ details_params.each_with_object(
+ event_list_details
+ ) do |(event_type, fields), hash|
+ selected_fields = Array(fields).reject(&:blank?).map(&:to_sym)
+ hash[event_type.to_sym] = selected_fields
+ end
+
+ # Removes details from the config which the user does not have permission to view
+ details_mask =
+ (
+ if show_pii
+ TimelineRecords::AVAILABLE_DETAILS_CONFIG_WITH_PII
+ else
+ TimelineRecords::AVAILABLE_DETAILS_CONFIG
+ end
+ )
+ (details_mask.keys & user_submitted_details.keys).index_with do |key|
+ details_mask[key] & user_submitted_details[key]
+ end
+ end
end
def pii_accessed?
- return false unless @show_pii
- detail_config = build_details_config
- detail_config.any? do |event_type, selected_fields|
+ return false unless show_pii
+ details_config.any? do |event_type, selected_fields|
pii_fields =
TimelineRecords::AVAILABLE_DETAILS_CONFIG_PII[event_type] || []
(selected_fields & pii_fields).any?
@@ -141,14 +182,14 @@ def pii_accessed?
end
def audit_pii_accessed?
- true if @show_pii && @event_names.include?("audits")
+ true if show_pii && @event_names.include?("audits")
end
def record_access_log_entry
return unless pii_accessed? || audit_pii_accessed?
details_accessed =
- build_details_config.reverse_merge(
+ details_config.reverse_merge(
@event_names.map { |key| [key.to_sym, []] }.to_h
)
details_accessed[:audits] = :accessed if details_accessed.key?(:audits)
diff --git a/app/controllers/patients/edit_controller.rb b/app/controllers/patients/edit_controller.rb
index 5dc1167114..967d0335c9 100644
--- a/app/controllers/patients/edit_controller.rb
+++ b/app/controllers/patients/edit_controller.rb
@@ -133,7 +133,7 @@ def set_eligible_schools
@eligible_schools =
current_team
.locations
- .where(type: %w[school generic_school])
+ .school
.joins(:location_year_groups)
.where(location_year_groups: { value: year_group })
.distinct
diff --git a/app/controllers/schools/base_controller.rb b/app/controllers/schools/base_controller.rb
index 5a6a819168..baa985c112 100644
--- a/app/controllers/schools/base_controller.rb
+++ b/app/controllers/schools/base_controller.rb
@@ -10,9 +10,9 @@ class Schools::BaseController < ApplicationController
def set_location
@location =
- policy_scope(Location).where(
- type: %w[school generic_school]
- ).find_by_urn_and_site!(params[:school_urn_and_site])
+ policy_scope(Location).school.find_by_urn_and_site!(
+ params[:school_urn_and_site]
+ )
authorize @location, policy_class: SchoolPolicy
end
diff --git a/app/controllers/schools_controller.rb b/app/controllers/schools_controller.rb
index cc10326f5f..c708a352fc 100644
--- a/app/controllers/schools_controller.rb
+++ b/app/controllers/schools_controller.rb
@@ -10,10 +10,7 @@ class SchoolsController < ApplicationController
def index
authorize Location, policy_class: SchoolPolicy
- locations =
- @form.apply(
- policy_scope(Location).school.or(policy_scope(Location).generic_school)
- )
+ locations = @form.apply(policy_scope(Location).school)
@pagy, @locations = pagy(locations)
diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb
index e353c31922..3c37afc359 100644
--- a/app/controllers/teams_controller.rb
+++ b/app/controllers/teams_controller.rb
@@ -29,7 +29,7 @@ def set_team
def set_schools
@schools =
@team
- .schools
+ .gias_schools
.joins(:team_locations)
.where(team_locations: { academic_year: AcademicYear.pending })
.distinct
diff --git a/app/helpers/consents_helper.rb b/app/helpers/consents_helper.rb
index 70cff3ec4f..852f539661 100644
--- a/app/helpers/consents_helper.rb
+++ b/app/helpers/consents_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ConsentsHelper
- ConsentRefusalOption = Struct.new(:value, :label, :divider)
+ ConsentRefusalOption = Struct.new(:value, :label, :hint, :divider)
def consent_refusal_reasons(consent)
reasons = %w[
@@ -16,10 +16,19 @@ def consent_refusal_reasons(consent)
reasons.insert(0, "contains_gelatine")
end
+ if consent.respond_to?(:location) && consent.location&.school?
+ reasons.insert(-2, "do_not_want_vaccination_at_school")
+ end
+
reasons.map do |value|
label = refusal_reason_label(consent, value)
+ hint =
+ I18n.t(
+ "activerecord.attributes.consent.reason_for_refusal_hints.#{value}",
+ default: nil
+ )
- ConsentRefusalOption.new(value:, label:, divider: value == "other")
+ ConsentRefusalOption.new(value:, label:, hint:, divider: value == "other")
end
end
diff --git a/app/helpers/patients_helper.rb b/app/helpers/patients_helper.rb
index 6e399e5d3a..1815e6d177 100644
--- a/app/helpers/patients_helper.rb
+++ b/app/helpers/patients_helper.rb
@@ -77,4 +77,25 @@ def patient_year_group(patient, academic_year:)
def patient_parents(patient)
format_parents_with_relationships(patient.parent_relationships)
end
+
+ def patient_needs_more_doses?(patient, programme, academic_year)
+ patient.programme_status(programme, academic_year:).needs_more_doses?
+ end
+
+ def patient_next_dose_label(patient, programme, academic_year)
+ patient
+ .programme_status(programme, academic_year:)
+ .dose_sequence
+ &.ordinalize
+ end
+
+ def patient_previous_dose_label(patient, programme, academic_year)
+ dose = patient.programme_status(programme, academic_year:).dose_sequence
+ (dose - 1).ordinalize if dose && dose > 1
+ end
+
+ def patient_short_name_possessive(patient)
+ name = patient.short_name
+ name.ends_with?("s") ? "#{name}’" : "#{name}’s"
+ end
end
diff --git a/app/helpers/programmes_helper.rb b/app/helpers/programmes_helper.rb
new file mode 100644
index 0000000000..050414ff34
--- /dev/null
+++ b/app/helpers/programmes_helper.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ProgrammesHelper
+ def programme_name_for_parents(programme)
+ nhs_name = programme.name_on_nhs_uk
+ if nhs_name
+ "#{programme.name_in_sentence} (#{nhs_name})"
+ else
+ programme.name_in_sentence
+ end
+ end
+
+ def programme_disease_names(programme)
+ programme
+ .disease_types
+ .map { I18n.t!(it, scope: :disease_types) }
+ .to_sentence
+ end
+end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index 162f7e8157..2abf45709d 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -73,4 +73,8 @@ def session_title(session)
def session_consent_style(session)
session.outbreak ? "Outbreak request" : "Standard request"
end
+
+ def session_future_dates(session)
+ session.future_dates.map { it.to_fs(:short_day_of_week) }.to_sentence
+ end
end
diff --git a/app/helpers/teams_helper.rb b/app/helpers/teams_helper.rb
new file mode 100644
index 0000000000..c63d5d1226
--- /dev/null
+++ b/app/helpers/teams_helper.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module TeamsHelper
+ include PhoneHelper
+
+ def team_contact_name(session: nil, vaccination_record: nil)
+ contact_entity(session:, vaccination_record:).name
+ end
+
+ def team_contact_email(session: nil, vaccination_record: nil)
+ contact_entity(session:, vaccination_record:).email
+ end
+
+ def team_contact_phone(session: nil, vaccination_record: nil)
+ format_phone_with_instructions(
+ contact_entity(session:, vaccination_record:)
+ )
+ end
+
+ private
+
+ def contact_entity(session: nil, vaccination_record: nil)
+ if session.nil? == vaccination_record.nil?
+ raise ArgumentError,
+ "provide either session: or vaccination_record:, not both or neither"
+ end
+
+ team_location =
+ session&.team_location || vaccination_record&.session&.team_location ||
+ vaccination_record
+ &.patient
+ &.school
+ &.team_locations
+ &.includes(:team, :subteam)
+ &.ordered
+ &.find_by(academic_year: AcademicYear.current)
+
+ team_location&.subteam || team_location&.team
+ end
+end
diff --git a/app/helpers/vaccination_records_helper.rb b/app/helpers/vaccination_records_helper.rb
index 227bab38f1..2e7fbc8504 100644
--- a/app/helpers/vaccination_records_helper.rb
+++ b/app/helpers/vaccination_records_helper.rb
@@ -1,6 +1,18 @@
# frozen_string_literal: true
module VaccinationRecordsHelper
+ def vaccination_record_date(vaccination_record)
+ vaccination_record.performed_at.to_date.to_fs(:uk_short)
+ end
+
+ def vaccination_record_today_or_date(vaccination_record)
+ if vaccination_record.performed_at.today?
+ "today"
+ else
+ "on #{vaccination_record.performed_at.to_date.to_fs(:long)}"
+ end
+ end
+
def vaccination_record_location(vaccination_record)
vaccination_record.location_name.presence ||
vaccination_record.location&.name || "Unknown"
diff --git a/app/helpers/vaccines_helper.rb b/app/helpers/vaccines_helper.rb
index 1075479590..e08745c0c9 100644
--- a/app/helpers/vaccines_helper.rb
+++ b/app/helpers/vaccines_helper.rb
@@ -4,4 +4,22 @@ module VaccinesHelper
def vaccine_heading(vaccine)
"#{vaccine.brand} (#{vaccine.programme.name})"
end
+
+ def vaccine_method(vaccine)
+ return unless vaccine
+
+ vaccine.human_enum_name(:method)&.downcase
+ end
+
+ def vaccine_side_effects_list(vaccine)
+ return if vaccine.nil?
+
+ vaccine
+ .side_effects
+ .map { Vaccine.human_enum_name(:side_effect, it) }
+ .sort
+ .uniq
+ .map { "- #{it}" }
+ .join("\n")
+ end
end
diff --git a/app/jobs/enqueue_patients_aged_out_of_schools_job.rb b/app/jobs/enqueue_patients_aged_out_of_schools_job.rb
index 06e232d83b..329fa2aa4a 100644
--- a/app/jobs/enqueue_patients_aged_out_of_schools_job.rb
+++ b/app/jobs/enqueue_patients_aged_out_of_schools_job.rb
@@ -5,7 +5,7 @@ class EnqueuePatientsAgedOutOfSchoolsJob < ApplicationJob
def perform
academic_year = AcademicYear.pending
- ids = Location.school.with_team(academic_year:).pluck(:id)
+ ids = Location.gias_school.with_team(academic_year:).pluck(:id)
PatientsAgedOutOfSchoolJob.perform_bulk(ids.zip)
end
end
diff --git a/app/jobs/enqueue_school_consent_reminders_job.rb b/app/jobs/enqueue_school_consent_reminders_job.rb
index 4811835da7..e3ca424490 100644
--- a/app/jobs/enqueue_school_consent_reminders_job.rb
+++ b/app/jobs/enqueue_school_consent_reminders_job.rb
@@ -5,7 +5,10 @@ class EnqueueSchoolConsentRemindersJob < ApplicationJob
def perform
sessions =
- Session.send_consent_reminders.joins(:location).merge(Location.school)
+ Session
+ .send_consent_reminders
+ .joins(:location)
+ .merge(Location.gias_school)
sessions.find_each do |session|
SendAutomaticSchoolConsentRemindersJob.perform_later(session)
diff --git a/app/jobs/enqueue_school_consent_requests_job.rb b/app/jobs/enqueue_school_consent_requests_job.rb
index b0190639c0..435f327366 100644
--- a/app/jobs/enqueue_school_consent_requests_job.rb
+++ b/app/jobs/enqueue_school_consent_requests_job.rb
@@ -5,7 +5,7 @@ class EnqueueSchoolConsentRequestsJob < ApplicationJob
def perform
sessions =
- Session.send_consent_requests.joins(:location).merge(Location.school)
+ Session.send_consent_requests.joins(:location).merge(Location.gias_school)
sessions.find_each do |session|
SendSchoolConsentRequestsJob.perform_later(session)
diff --git a/app/jobs/enqueue_school_session_reminders_job.rb b/app/jobs/enqueue_school_session_reminders_job.rb
index 44f6424a46..7a50cda3e0 100644
--- a/app/jobs/enqueue_school_session_reminders_job.rb
+++ b/app/jobs/enqueue_school_session_reminders_job.rb
@@ -9,7 +9,7 @@ def perform
.includes(:session_programme_year_groups)
.has_date(Date.tomorrow)
.joins(:location)
- .merge(Location.school)
+ .merge(Location.gias_school)
sessions.find_each do |session|
SendSchoolSessionRemindersJob.perform_later(session)
diff --git a/app/jobs/patients_aged_out_of_school_job.rb b/app/jobs/patients_aged_out_of_school_job.rb
index 9e486d1192..6f820320d4 100644
--- a/app/jobs/patients_aged_out_of_school_job.rb
+++ b/app/jobs/patients_aged_out_of_school_job.rb
@@ -11,7 +11,10 @@ def perform(school_id)
academic_year = AcademicYear.pending
school =
- Location.school.includes(:location_year_groups).find_by(id: school_id)
+ Location
+ .gias_school
+ .includes(:location_year_groups)
+ .find_by(id: school_id)
return if school.nil?
diff --git a/app/jobs/search_vaccination_records_in_nhs_job.rb b/app/jobs/search_vaccination_records_in_nhs_job.rb
index 0ea7116ad2..dbc56a9a9f 100644
--- a/app/jobs/search_vaccination_records_in_nhs_job.rb
+++ b/app/jobs/search_vaccination_records_in_nhs_job.rb
@@ -20,7 +20,7 @@ def perform(patient_id)
return unless feature_flags_enabled
- existing_vaccination_records.find_each do |vaccination_record|
+ existing_vaccination_records.each do |vaccination_record|
incoming_vaccination_record =
incoming_vaccination_records.find do
it.nhs_immunisations_api_id ==
@@ -28,11 +28,15 @@ def perform(patient_id)
end
if incoming_vaccination_record
- vaccination_record.update!(
+ vaccination_record.assign_attributes(
incoming_vaccination_record
.attributes
.except("id", "uuid", "created_at")
- .merge(updated_at: Time.current)
+ .merge(
+ duplicate_of_vaccination_record:
+ incoming_vaccination_record.duplicate_of_vaccination_record,
+ updated_at: Time.current
+ )
)
incoming_vaccination_records.delete(incoming_vaccination_record)
@@ -41,10 +45,12 @@ def perform(patient_id)
end
end
- # Remaining incoming_vaccination_records are new.
# Save non-discarded records first so they have IDs before discarded
# duplicates reference them via duplicate_of_vaccination_record_id.
- incoming_vaccination_records.sort_by { it.discarded? ? 1 : 0 }.each(&:save!)
+ (
+ existing_vaccination_records.reject(&:destroyed?) +
+ incoming_vaccination_records
+ ).sort_by { it.discarded? ? 1 : 0 }.each(&:save!)
update_vaccination_search_timestamps if patient.nhs_number.present?
@@ -104,6 +110,7 @@ def existing_vaccination_records
@existing_vaccination_records ||=
patient
.vaccination_records
+ .with_discarded
.includes(:identity_check)
.sourced_from_nhs_immunisations_api
.for_programmes(programmes)
@@ -126,6 +133,7 @@ def deduplicate_vaccination_records(incoming_vaccination_records)
service_vaccination_records =
patient
.vaccination_records
+ .administered
.with_correct_source_for_nhs_immunisations_api
.includes(:team)
@@ -150,8 +158,23 @@ def deduplicate_vaccination_records(incoming_vaccination_records)
end
elsif records.any?(&:nhs_immunisations_api_primary_source)
# If some records have `primarySource: true`, set `discarded_at` for all `primarySource: false` records,
- # pointing each at the first `primarySource: true` record
- canonical = records.find(&:nhs_immunisations_api_primary_source)
+ # pointing each at the first `primarySource: true` record.
+
+ canonical_incoming =
+ records.find(&:nhs_immunisations_api_primary_source)
+ canonical_existing =
+ patient
+ .vaccination_records
+ .sourced_from_nhs_immunisations_api
+ .with_discarded
+ .find_by(
+ nhs_immunisations_api_id:
+ canonical_incoming.nhs_immunisations_api_id
+ )
+
+ # Prefer the persisted DB record (if it already exists from a previous run) so that
+ # `duplicate_of_vaccination_record_id` is non-nil when the non-primary record is saved.
+ canonical = canonical_existing || canonical_incoming
records
.select(&:sourced_from_nhs_immunisations_api?)
.reject(&:nhs_immunisations_api_primary_source)
diff --git a/app/lib/feature_flag_factory.rb b/app/lib/feature_flag_factory.rb
index 33a4a3966a..8ec9c52e57 100644
--- a/app/lib/feature_flag_factory.rb
+++ b/app/lib/feature_flag_factory.rb
@@ -16,7 +16,11 @@ def self.call
end
end
- FEATURES_FOR_DEVELOPMENT = %i[dev_tools testing_api].freeze
+ FEATURES_FOR_DEVELOPMENT = %i[
+ dev_tools
+ testing_api
+ vaccinating_16_plus_year_olds
+ ].freeze
def self.enable_for_development!(check_rails_env: true)
if check_rails_env && !(Rails.env.development? || Rails.env.end_to_end?)
diff --git a/app/lib/fhir_mapper/location.rb b/app/lib/fhir_mapper/location.rb
index ed6d0aa5af..07b515d3cb 100644
--- a/app/lib/fhir_mapper/location.rb
+++ b/app/lib/fhir_mapper/location.rb
@@ -2,7 +2,7 @@
module FHIRMapper
class Location
- delegate :school?, :clinic?, :type, :urn, :ods_code, to: :@location
+ delegate :gias_school?, :clinic?, :type, :urn, :ods_code, to: :@location
def initialize(location)
@location = location
@@ -14,7 +14,7 @@ class UnknownValueError < StandardError
end
def fhir_reference
- if school?
+ if gias_school?
value = urn || UNKNOWN_IDENTIFIER
system = "https://fhir.hl7.org.uk/Id/urn-school-number"
elsif clinic?
diff --git a/app/lib/fhir_mapper/vaccination_record.rb b/app/lib/fhir_mapper/vaccination_record.rb
index df945af9b5..e09ac3373c 100644
--- a/app/lib/fhir_mapper/vaccination_record.rb
+++ b/app/lib/fhir_mapper/vaccination_record.rb
@@ -14,6 +14,9 @@ class VaccinationRecord
BATCH_EXPIRY_MIN = Date.new(Date.current.year - 100, 1, 1)
BATCH_EXPIRY_MAX = Date.new(Date.current.year + 100, 1, 1)
+ VACCINATION_PROCEDURE_EXTENSION_URL =
+ "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure"
+
def initialize(vaccination_record)
@vaccination_record = vaccination_record
end
@@ -45,7 +48,8 @@ def fhir_record
sourced_from_service? || sourced_from_national_reporting?
immunisation.manufacturer = vaccine.fhir_manufacturer_reference
- immunisation.location = (location || ::Location.school.new).fhir_reference
+ immunisation.location =
+ (location || ::Location.gias_school.new).fhir_reference
immunisation.lotNumber = batch_number
immunisation.expirationDate = batch_expiry.to_s
immunisation.site = fhir_site
@@ -82,6 +86,18 @@ def self.from_fhir_record(fhir_record, patient:)
.sole
.value
attrs[:nhs_immunisations_api_primary_source] = fhir_record.primarySource
+ recorded = fhir_record.recorded
+ attrs[:nhs_immunisations_api_recorded_at] = Time.zone.parse(
+ recorded
+ ) if recorded
+
+ procedure_coding = vaccination_procedure_coding_from_fhir(fhir_record)
+ attrs[
+ :nhs_immunisations_api_snomed_procedure_code
+ ] = procedure_coding&.code
+ attrs[
+ :nhs_immunisations_api_snomed_procedure_term
+ ] = procedure_coding&.display
attrs[:programme] = Programme.from_fhir_record(fhir_record)
@@ -109,11 +125,16 @@ def self.from_fhir_record(fhir_record, patient:)
)
end
- performer_ods_code = org_performer_ods_code_from_fhir(fhir_record)
+ org_actor = org_performer_actor_from_fhir(fhir_record)
+ performer_ods_code = org_actor&.identifier&.value
unless performer_ods_code == FHIRMapper::Location::UNKNOWN_IDENTIFIER
attrs[:performed_ods_code] = performer_ods_code
end
+ if org_actor&.display.present?
+ notes << "Performing organisation display name: #{org_actor.display}"
+ end
+
user_performer_name = user_performer_name_from_fhir(fhir_record)
attrs[:performed_by_given_name] = user_performer_name&.given&.first
attrs[:performed_by_family_name] = user_performer_name&.family
@@ -122,13 +143,27 @@ def self.from_fhir_record(fhir_record, patient:)
attrs[:delivery_site] = site_from_fhir(fhir_record)
dose_sequence = dose_sequence_from_fhir(fhir_record)
- max_dose_sequence = attrs[:programme].maximum_dose_sequence
- if dose_sequence.present? && dose_sequence > max_dose_sequence
- notes << "Reported dose sequence: #{dose_sequence}"
+ if dose_sequence
+ if dose_sequence > attrs[:programme].maximum_dose_sequence ||
+ dose_sequence < 1
+ notes << "Reported dose number: #{dose_sequence}"
+ else
+ attrs[:dose_sequence] = dose_sequence
+ end
else
- attrs[:dose_sequence] = dose_sequence
+ notes << dose_number_string_note_from_fhir(fhir_record)
end
+ reason_coding = reason_coding_from_fhir(fhir_record)
+ attrs[:nhs_immunisations_api_snomed_reason_code] = reason_coding&.code
+ attrs[:nhs_immunisations_api_snomed_reason_term] = reason_coding&.display
+
+ product_coding = vaccine_product_coding_from_fhir(fhir_record)
+ attrs[:nhs_immunisations_api_snomed_product_code] = product_coding&.code
+ attrs[
+ :nhs_immunisations_api_snomed_product_term
+ ] = product_coding&.display
+
attrs[:vaccine] = Vaccine.from_fhir_record(fhir_record)
attrs[:batch_number] = fhir_record.lotNumber&.to_s
@@ -145,7 +180,6 @@ def self.from_fhir_record(fhir_record, patient:)
)
else
attrs[:disease_types] = attrs[:programme].disease_types
- notes << vaccine_batch_notes_from_fhir(fhir_record)
attrs[:full_dose] = true
end
@@ -170,8 +204,7 @@ def fhir_identifier
def fhir_vaccination_procedure_extension
FHIR::Extension.new(
- url:
- "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure",
+ url: VACCINATION_PROCEDURE_EXTENSION_URL,
valueCodeableConcept: vaccine.fhir_procedure_coding(dose_sequence:)
)
end
@@ -285,21 +318,6 @@ def fhir_dose_quantity
end
end
- private_class_method def self.vaccine_batch_notes_from_fhir(fhir_record)
- fhir_vaccine =
- fhir_record.vaccineCode&.coding&.find do
- it.system == "http://snomed.info/sct"
- end
-
- vaccine_snomed_code = fhir_vaccine&.code
- vaccine_description = fhir_vaccine&.display.presence
-
- [
- ("SNOMED product code: #{vaccine_snomed_code}" if vaccine_snomed_code),
- ("SNOMED description: #{vaccine_description}" if vaccine_description)
- ].compact.join("\n").presence
- end
-
def fhir_user_performer(reference_id:)
FHIR::Immunization::Performer.new(
actor: FHIR::Reference.new(reference: "##{reference_id}")
@@ -327,10 +345,31 @@ def fhir_org_performer
)
end
- private_class_method def self.org_performer_ods_code_from_fhir(fhir_record)
- org_actor =
- fhir_record.performer.find { it.actor&.type == "Organization" }&.actor
- org_actor&.identifier&.value
+ private_class_method def self.org_performer_actor_from_fhir(fhir_record)
+ fhir_record.performer.find { it.actor&.type == "Organization" }&.actor
+ end
+
+ private_class_method def self.vaccination_procedure_coding_from_fhir(
+ fhir_record
+ )
+ fhir_record
+ .extension
+ &.find { it.url == VACCINATION_PROCEDURE_EXTENSION_URL }
+ &.valueCodeableConcept
+ &.coding
+ &.find { it.system == "http://snomed.info/sct" }
+ end
+
+ private_class_method def self.reason_coding_from_fhir(fhir_record)
+ fhir_record.reasonCode&.first&.coding&.find do
+ it.system == "http://snomed.info/sct"
+ end
+ end
+
+ private_class_method def self.vaccine_product_coding_from_fhir(fhir_record)
+ fhir_record.vaccineCode&.coding&.find do
+ it.system == "http://snomed.info/sct"
+ end
end
def fhir_reason_code
@@ -350,10 +389,21 @@ def fhir_protocol_applied
end
private_class_method def self.dose_sequence_from_fhir(fhir_record)
- # TODO: currently we only look at `doseNumberPositiveInt` but often `doseNumberString` is populated instead
- # This doesn't matter much for flu, but this may need to be revisited when we start consuming programmes
- # where dose number matters more (eg MMR)
- fhir_record.protocolApplied.sole.doseNumberPositiveInt
+ protocol = fhir_record.protocolApplied&.sole
+
+ if protocol&.doseNumberPositiveInt.present?
+ return protocol&.doseNumberPositiveInt
+ end
+
+ Integer(protocol&.doseNumberString, exception: false)
+ end
+
+ private_class_method def self.dose_number_string_note_from_fhir(fhir_record)
+ dose_string = fhir_record.protocolApplied&.sole&.doseNumberString
+
+ return if dose_string.blank?
+
+ "Reported dose number string: #{dose_string}"
end
end
end
diff --git a/app/lib/generate/cohort_imports.rb b/app/lib/generate/cohort_imports.rb
index 3e120fb1cd..270ad3df5e 100644
--- a/app/lib/generate/cohort_imports.rb
+++ b/app/lib/generate/cohort_imports.rb
@@ -15,7 +15,7 @@ def initialize(
)
@team = team
@programmes = programmes.presence || team.programmes
- @urns = urns || @team.schools.pluck(:urn)
+ @urns = urns || @team.gias_schools.pluck(:urn)
@school_year_groups = school_year_groups
@patient_count = patient_count
@progress_bar = progress_bar
diff --git a/app/lib/generate/consents.rb b/app/lib/generate/consents.rb
index b25267e1e4..ad34c5b206 100644
--- a/app/lib/generate/consents.rb
+++ b/app/lib/generate/consents.rb
@@ -48,7 +48,7 @@ def patients
:session_programme_year_groups,
:team_location
)
- .merge(Location.school)
+ .merge(Location.gias_school)
.has_all_programmes_of([programme])
end
@@ -79,7 +79,7 @@ def session_for(patient)
.joins(
"INNER JOIN locations ON locations.id = team_locations.location_id"
)
- .merge(Location.school)
+ .merge(Location.gias_school)
.has_all_programmes_of([programme])
.sample
end
@@ -97,7 +97,8 @@ def create_consents(response, count)
consent_forms =
available_patient_sessions.map do |patient, session|
- school = session.location.school? ? session.location : patient.school
+ school =
+ session.location.gias_school? ? session.location : patient.school
@updated_patients << patient
@@ -124,7 +125,7 @@ def validate_programme_and_session(programme, session)
end
elsif Session
.has_any_programmes_of([programme])
- .none? { it.location.school? }
+ .none? { it.location.gias_school? }
raise "Programme #{programme.type} does not have a school session"
end
end
diff --git a/app/lib/gias.rb b/app/lib/gias.rb
index 3403db702f..ba348f105e 100644
--- a/app/lib/gias.rb
+++ b/app/lib/gias.rb
@@ -66,7 +66,7 @@ def import(
next if gias_establishment_number.blank?
schools << Location.new(
- type: :school,
+ type: "gias_school",
urn: row["URN"],
gias_local_authority_code: row["LA (code)"],
gias_establishment_number:,
@@ -110,7 +110,7 @@ def check_import(input_file: DEFAULT_FILE_PATH, progress_bar: nil)
existing:
Set.new(
Location
- .school
+ .gias_school
.joins(:sessions)
.merge(Session.scheduled)
.pluck(:urn)
@@ -131,12 +131,12 @@ def check_import(input_file: DEFAULT_FILE_PATH, progress_bar: nil)
}
}
- existing_schools = Set.new(Location.school.pluck(:urn))
+ existing_schools = Set.new(Location.gias_school.pluck(:urn))
team_schools =
Set.new(
TeamLocation
.joins(:location)
- .merge(Location.school)
+ .merge(Location.gias_school)
.pluck(:"locations.urn")
)
@@ -394,7 +394,7 @@ def check_for_year_group_changes(row, school_set, existing_schools)
return unless urn.in? existing_schools
new_year_groups = process_year_groups(row)
- current_year_groups = Location.school.find_by(urn:).gias_year_groups
+ current_year_groups = Location.gias_school.find_by(urn:).gias_year_groups
if new_year_groups != current_year_groups
school_set[:year_group_changes][urn] = {
@@ -415,7 +415,7 @@ def calculate_percentage(schools_set, metric)
def format_successors_with_teams(successor_urns)
annotated_successor_urns =
successor_urns.map do |successor_urn|
- locations = Location.school.where(urn: successor_urn)
+ locations = Location.gias_school.where(urn: successor_urn)
if locations.count == 1
teams = locations.sole.teams.uniq
diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb
index a0b7e0f7bb..58d9f4e230 100644
--- a/app/lib/govuk_notify_personalisation.rb
+++ b/app/lib/govuk_notify_personalisation.rb
@@ -3,8 +3,13 @@
class GovukNotifyPersonalisation
include Rails.application.routes.url_helpers
+ include PatientsHelper
include PhoneHelper
+ include ProgrammesHelper
+ include SessionsHelper
+ include TeamsHelper
include VaccinationRecordsHelper
+ include VaccinesHelper
def initialize(
academic_year: nil,
@@ -61,57 +66,53 @@ def initialize(
:team_location,
:vaccination_record
- def batch_name
- vaccination_record&.batch_number
- end
-
- def consent_deadline
- session&.consent_deadline_date&.to_fs(:short_day_of_week)
- end
-
- def consent_link
- return nil if (session.nil? && team_location.nil?) || programmes.empty?
-
- programme_params = programmes.flat_map { it.variant_for(patient:).to_param }
-
- host +
- start_parent_interface_consent_forms_path(
- session || team_location,
- programme_params.join("-")
- )
- end
-
- def consented_vaccine_methods_message
- return if consent.nil? && consent_form.nil?
-
- consent_form_programmes =
- (consent ? [consent] : consent_form.consent_form_programmes)
-
- programmes = consent_form_programmes.map(&:programme)
+ delegate :has_multiple_dates?,
+ :next_or_today_session_date,
+ :next_or_today_session_dates,
+ :next_or_today_session_dates_or,
+ :next_session_date,
+ :next_session_dates,
+ :next_session_dates_or,
+ :subsequent_session_dates_offered_message,
+ to: :session_dates_presenter
+
+ delegate :consent_deadline,
+ :consent_link,
+ :consented_vaccine_methods_message,
+ :follow_up_discussion,
+ :reason_for_refusal,
+ :survey_deadline_date,
+ :talk_to_your_child_message,
+ to: :consent_details_presenter
+
+ delegate :is_catch_up?,
+ :outcome_administered?,
+ :outcome_not_administered?,
+ :reason_did_not_vaccinate,
+ :show_additional_instructions?,
+ :vaccination,
+ :vaccination_and_dates,
+ :vaccination_and_dates_sms,
+ :vaccination_and_method,
+ :vaccine,
+ :vaccine_and_dose,
+ :vaccine_and_method,
+ :vaccine_is?,
+ :vaccine_side_effects,
+ to: :vaccination_details_presenter
+
+ delegate :invitation_to_clinic_custom_mmr_message,
+ :invitation_to_clinic_generic_message,
+ :mmr_or_mmrv_vaccine,
+ :mmr_programme,
+ :mmr_second_dose_required?,
+ :next_mmr_dose_date,
+ :patient_on_last_dose?,
+ to: :mmr_details_presenter
+
+ delegate :delay_vaccination_review_context, to: :triage_details_presenter
- consented_vaccine_methods =
- if programmes.any?(&:has_multiple_vaccine_methods?)
- if consent_form_programmes.any?(&:vaccine_method_injection_and_nasal?)
- "nasal spray flu vaccine, or the injected flu vaccine if the nasal spray is not suitable"
- elsif consent_form_programmes.any?(&:vaccine_method_nasal?)
- "nasal spray flu vaccine"
- else
- "injected flu vaccine"
- end
- elsif programmes.any?(&:vaccine_may_contain_gelatine?)
- if consent_form_programmes.any?(&:without_gelatine)
- "vaccine without gelatine"
- end
- end
-
- return "" if consented_vaccine_methods.nil?
-
- "You’ve agreed for #{short_patient_name} to have the #{consented_vaccine_methods}."
- end
-
- def day_month_year_of_vaccination
- vaccination_record&.performed_at&.to_date&.to_fs(:uk_short)
- end
+ delegate :privacy_notice_url, :privacy_policy_url, to: :team, prefix: true
def full_and_preferred_patient_name
(consent_form || patient).full_name_with_known_as(context: :parents)
@@ -135,195 +136,10 @@ def location_name
end
end
- def mmr_second_dose_message
- return unless patient
- return unless mmr_programme
-
- programme_status = patient.programme_status(mmr_programme, academic_year:)
-
- return "" if programme_status.vaccinated?
-
- [
- "## Your child still needs a second dose of the MMR vaccine",
- "To be fully protected against measles, mumps and rubella, your " \
- "child needs a second dose of the vaccine. Our team will be in " \
- "touch about this soon."
- ].join("\n\n")
- end
-
- def mmr_second_dose_required?
- mmr_programme.present? && patient_on_last_dose?
- end
-
- def mmr_second_dose_required
- mmr_second_dose_required?
- end
-
- def invitation_to_clinic_generic_message
- [
- (
- if mmr_second_dose_required
- "If you would like your local GP surgery to give #{short_patient_name} " \
- "their 2nd dose, contact the surgery in the usual way."
- end
- ),
- "#{mmr_second_dose_required ? "Alternatively, they" : "They"} can have this vaccination " \
- "at a community clinic. If you’d like to book a clinic appointment, please contact " \
- "us using the details below.",
- (mmr_second_dose_waiting_period_message if mmr_second_dose_required)
- ].compact.join("\n\n")
- end
-
- def invitation_to_clinic_custom_mmr_message
- return "" unless mmr_second_dose_required
-
- case team&.organisation&.ods_code
- when "RT5" # Leicestershire Partnership Trust (LPT)
- [
- mmr_second_dose_waiting_period_message,
- "It’s also possible for #{short_patient_name} to be vaccinated at your local GP surgery. " \
- "To book an appointment, contact the surgery in the usual way."
- ].join("\n\n")
- when "RYG" # Coventry & Warwickshire Partnership NHS Trust (CWPT)
- [
- mmr_second_dose_waiting_period_message,
- "## You have 2 options for booking the vaccination",
- "You can ask your local GP surgery to give #{short_patient_name} their 2nd dose. " \
- "To book an appointment, contact the surgery in the usual way."
- ].join("\n\n")
- end
- end
-
- def mmr_second_dose_waiting_period_message
- "It’s important to wait at least 28 days after the 1st dose of an MMR or " \
- "MMRV vaccination before getting the 2nd dose. #{short_patient_name} " \
- "should not get the 2nd dose until #{next_mmr_dose_date}. Please keep this in " \
- "mind when booking the appointment."
- end
-
- def next_mmr_dose_date
- return if patient.nil?
- return if mmr_programme.nil?
-
- patient
- .programme_status(mmr_programme, academic_year:)
- .next_dose_eligible_date
- &.to_fs(:long)
- end
-
- def patient_on_last_dose?
- return unless patient
- return if mmr_programme.nil?
-
- patient.reload.programme_status(mmr_programme, academic_year:).on_last_dose?
- end
-
- def mmr_or_mmrv_vaccine
- if mmr_programme.present?
- if mmr_programme.variant_type == "mmrv"
- "MMR or MMRV vaccine"
- else
- "MMR vaccine"
- end
- end
- end
-
- def mmr_programme
- @mmr_programme ||= programmes.find(&:mmr?)
- end
-
- def delay_vaccination_review_context
- return if patient.nil? || session.nil?
-
- latest_delayed_triage =
- patient.latest_delayed_triage(programme_types: session.programme_types)
-
- return if latest_delayed_triage.nil?
-
- session_date = session.next_date(include_today: true)
- triage_date = latest_delayed_triage.created_at.to_date
-
- if session_date && triage_date == session_date
- "assessed #{short_patient_name} in the vaccination session"
- else
- "reviewed the answers you gave to the health questions about #{short_patient_name}"
- end
- end
-
- def next_or_today_session_date
- return "" unless session_dates_are_accurate?
-
- session&.next_date(include_today: true)&.to_fs(:short_day_of_week)
- end
-
- def next_or_today_session_dates
- return "" unless session_dates_are_accurate?
-
- session
- &.today_or_future_dates
- &.map { it.to_fs(:short_day_of_week) }
- &.to_sentence
- end
-
- def next_or_today_session_dates_or
- return "" unless session_dates_are_accurate?
-
- session
- &.today_or_future_dates
- &.map { it.to_fs(:short_day_of_week) }
- &.to_sentence(last_word_connector: ", or ", two_words_connector: " or ")
- end
-
- def next_session_date
- return "" unless session_dates_are_accurate?
-
- session&.next_date(include_today: false)&.to_fs(:short_day_of_week)
- end
-
- def next_session_dates
- return "" unless session_dates_are_accurate?
-
- session&.future_dates&.map { it.to_fs(:short_day_of_week) }&.to_sentence
- end
-
- def next_session_dates_or
- return "" unless session_dates_are_accurate?
-
- session
- &.future_dates
- &.map { it.to_fs(:short_day_of_week) }
- &.to_sentence(last_word_connector: ", or ", two_words_connector: " or ")
- end
-
- def outcome_not_administered?
- vaccination_record.nil? || !outcome_administered?
- end
-
def patient_date_of_birth
patient&.date_of_birth&.to_fs(:long)
end
- def reason_did_not_vaccinate
- return if vaccination_record.nil? || vaccination_record.administered?
-
- I18n.t(
- vaccination_record.outcome,
- scope: "mailers.vaccination_mailer.reasons_did_not_vaccinate",
- short_patient_name:
- )
- end
-
- def follow_up_discussion
- consent_form&.follow_up_requested
- end
-
- def reason_for_refusal
- reason = consent_form&.reason_for_refusal || consent&.reason_for_refusal
- return if reason.nil?
-
- I18n.t(reason, scope: "mailers.consent_form_mailer.reasons_for_refusal")
- end
-
def short_patient_name
(consent_form || patient)&.short_name
end
@@ -334,20 +150,6 @@ def short_patient_name_apos
short_patient_name + apos
end
- def show_additional_instructions? =
- vaccination_record.present? && !vaccination_record.already_had?
-
- def subsequent_session_dates_offered_message
- return nil if session.nil?
-
- dates = session.future_dates.drop(1)
- return "" if dates.empty?
-
- "If they’re not seen, they’ll be offered the vaccination on #{
- dates.map { it.to_fs(:short_day_of_week) }.to_sentence
- }."
- end
-
def subteam_email = (subteam || team).email
def subteam_name = (subteam || team).name
@@ -356,193 +158,29 @@ def subteam_phone
format_phone_with_instructions(subteam || team)
end
- def survey_deadline_date
- recorded_at = consent_form&.recorded_at || consent&.created_at
- return if recorded_at.nil?
-
- (recorded_at + 7.days).to_date.to_fs(:long)
- end
-
- def talk_to_your_child_message
- return nil if patient.nil?
- return "" if patient_year_group <= 6
-
- [
- "## Talk to your child about what they want",
- "We suggest you talk to your child about the vaccination before you respond to us. " \
- "Young people have the right to refuse vaccinations.",
- "They also have [the right to consent to their own vaccinations]" \
- "(https://www.nhs.uk/conditions/consent-to-treatment/children/) " \
- "if they show they fully understand what’s involved. Our team might give young " \
- "people this opportunity if they assess them as suitably competent."
- ].join("\n\n")
- end
-
- delegate :privacy_notice_url, :privacy_policy_url, to: :team, prefix: true
-
- def today_or_date_of_vaccination
- return if vaccination_record.nil?
-
- if vaccination_record.performed_at.today?
- "today"
- else
- "on #{vaccination_record.performed_at.to_date.to_fs(:long)}"
- end
- end
-
- def vaccination
- if vaccination_record.present?
- # We're sending communication about a specific vaccination that took place.
- "#{programme_names.to_sentence} vaccination".pluralize(
- programme_names.length
- )
- else
- # We're sending about a vaccination that will take place.
- names = programme_names
-
- if mmr_second_dose_required
- names = names.map { it == "MMR" ? "2nd dose of the MMR" : it }
- end
-
- "#{names.to_sentence} vaccination".pluralize(names.length)
- end
- end
-
- def vaccination_and_dates
- if next_or_today_session_dates_or.present?
- "#{vaccination} on #{next_or_today_session_dates_or}"
- else
- vaccination
- end
- end
-
- # TODO: Remove this method when schools start offering MMRV.
- def vaccination_and_dates_sms
- if next_or_today_session_dates_or.present?
- "#{vaccination} on #{next_or_today_session_dates_or}"
- else
- vaccination
- end
- end
-
- def vaccination_and_method
- "#{programme_names_and_methods.to_sentence} vaccination".pluralize(
- programme_names_and_methods.length
- )
- end
-
- def vaccine
- "#{programme_names.to_sentence} vaccine".pluralize(programme_names.length)
- end
-
- def vaccine_and_dose
- if (dose_sequence = vaccination_record&.dose_sequence)
- "#{programme_names.to_sentence} #{dose_sequence.ordinalize} dose"
- else
- programme_names.to_sentence
- end
- end
-
- def vaccine_and_method
- "#{programme_names_and_methods.to_sentence} vaccine".pluralize(
- programme_names_and_methods.length
- )
- end
-
- def vaccine_brand
- vaccination_record&.vaccine&.brand
- end
-
- def vaccine_is?(method)
- if vaccination_record
- vaccination_record.vaccine&.method == method
- elsif programmes.present?
- if patient
- programmes.any? do |programme|
- patient.vaccine_criteria(programme:, academic_year:).primary_method ==
- method
- end
- else
- Vaccine.for_programmes(programmes).exists?(method:)
- end
- end
- end
-
- def vaccine_side_effects
- side_effects =
- if vaccination_record
- vaccination_record.vaccine&.side_effects
- elsif programmes.present?
- if patient
- programmes.flat_map do |programme|
- patient.vaccine_criteria(programme:, academic_year:).side_effects
- end
- else
- Vaccine.for_programmes(programmes).active.flat_map(&:side_effects)
- end
- end
-
- return if side_effects.nil?
-
- descriptions =
- side_effects.map { Vaccine.human_enum_name(:side_effect, it) }.sort.uniq
-
- descriptions.map { "- #{it}" }.join("\n")
- end
-
- def is_catch_up?
- return false if patient.nil? || programmes.empty?
-
- @is_catch_up ||=
- programmes.any? { it.is_catch_up?(year_group: patient_year_group) }
- end
-
- def has_multiple_dates?
- return false if session.nil?
-
- session.future_dates.length > 1
- end
-
- def outcome_administered?
- vaccination_record.nil? || vaccination_record.administered?
- end
-
private
def session_dates_are_accurate?
consent_form ? consent_form.session_dates_are_accurate? : true
end
- def patient_year_group
- @patient_year_group ||= patient.year_group(academic_year:)
+ def session_dates_presenter
+ @session_dates_presenter ||= SessionDatesPresenter.new(self)
end
- def programme_names
- @programme_names ||= programmes.map(&:name_in_sentence)
+ def consent_details_presenter
+ @consent_details_presenter ||= ConsentDetailsPresenter.new(self)
end
- def programme_names_and_methods
- @programme_names_and_methods ||=
- programmes.map do |programme|
- if programme.has_multiple_vaccine_methods?
- vaccine_method =
- if vaccination_record
- Vaccine.delivery_method_to_vaccine_method(
- vaccination_record.delivery_method
- )
- elsif patient
- patient.vaccine_criteria(
- programme:,
- academic_year:
- ).primary_method
- end
+ def vaccination_details_presenter
+ @vaccination_details_presenter ||= VaccinationDetailsPresenter.new(self)
+ end
- method_prefix =
- Vaccine.human_enum_name(:method_prefix, vaccine_method)
- "#{method_prefix} #{programme.name_in_sentence}".lstrip
- else
- programme.name_in_sentence
- end
- end
+ def mmr_details_presenter
+ @mmr_details_presenter ||= MmrDetailsPresenter.new(self)
+ end
+
+ def triage_details_presenter
+ @triage_details_presenter ||= TriageDetailsPresenter.new(self)
end
end
diff --git a/app/lib/govuk_notify_personalisation/consent_details_presenter.rb b/app/lib/govuk_notify_personalisation/consent_details_presenter.rb
new file mode 100644
index 0000000000..b2335ca4f5
--- /dev/null
+++ b/app/lib/govuk_notify_personalisation/consent_details_presenter.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+class GovukNotifyPersonalisation
+ class ConsentDetailsPresenter
+ include Rails.application.routes.url_helpers
+
+ def initialize(personalisation)
+ @personalisation = personalisation
+ end
+
+ attr_reader :personalisation
+
+ delegate :consent,
+ :consent_form,
+ :host,
+ :patient,
+ :programmes,
+ :session,
+ :short_patient_name,
+ :team_location,
+ to: :personalisation
+
+ def consent_deadline
+ session&.consent_deadline_date&.to_fs(:short_day_of_week)
+ end
+
+ def consent_link
+ return nil if (session.nil? && team_location.nil?) || programmes.empty?
+
+ programme_params =
+ programmes.flat_map { it.variant_for(patient:).to_param }
+
+ host +
+ start_parent_interface_consent_forms_path(
+ session || team_location,
+ programme_params.join("-")
+ )
+ end
+
+ def consented_vaccine_methods_message
+ return if consent.nil? && consent_form.nil?
+
+ consent_form_programmes =
+ (consent ? [consent] : consent_form.consent_form_programmes)
+
+ consent_programmes = consent_form_programmes.map(&:programme)
+
+ consented_vaccine_methods =
+ if consent_programmes.any?(&:has_multiple_vaccine_methods?)
+ if consent_form_programmes.any?(&:vaccine_method_injection_and_nasal?)
+ "nasal spray flu vaccine, or the injected flu vaccine if the nasal spray is not suitable"
+ elsif consent_form_programmes.any?(&:vaccine_method_nasal?)
+ "nasal spray flu vaccine"
+ else
+ "injected flu vaccine"
+ end
+ elsif consent_programmes.any?(&:vaccine_may_contain_gelatine?)
+ if consent_form_programmes.any?(&:without_gelatine)
+ "vaccine without gelatine"
+ end
+ end
+
+ return "" if consented_vaccine_methods.nil?
+
+ "You’ve agreed for #{short_patient_name} to have the #{consented_vaccine_methods}."
+ end
+
+ def follow_up_discussion
+ consent_form&.follow_up_requested
+ end
+
+ def reason_for_refusal
+ reason = consent_form&.reason_for_refusal || consent&.reason_for_refusal
+ return if reason.nil?
+
+ I18n.t(reason, scope: "mailers.consent_form_mailer.reasons_for_refusal")
+ end
+
+ def survey_deadline_date
+ recorded_at = consent_form&.recorded_at || consent&.created_at
+ return if recorded_at.nil?
+
+ (recorded_at + 7.days).to_date.to_fs(:long)
+ end
+
+ def talk_to_your_child_message
+ return nil if patient.nil?
+ return "" if patient.year_group(academic_year:) <= 6
+
+ [
+ "## Talk to your child about what they want",
+ "We suggest you talk to your child about the vaccination before you respond to us. " \
+ "Young people have the right to refuse vaccinations.",
+ "They also have [the right to consent to their own vaccinations]" \
+ "(https://www.nhs.uk/conditions/consent-to-treatment/children/) " \
+ "if they show they fully understand what’s involved. Our team might give young " \
+ "people this opportunity if they assess them as suitably competent."
+ ].join("\n\n")
+ end
+
+ private
+
+ def academic_year
+ personalisation.academic_year
+ end
+ end
+end
diff --git a/app/lib/govuk_notify_personalisation/mmr_details_presenter.rb b/app/lib/govuk_notify_personalisation/mmr_details_presenter.rb
new file mode 100644
index 0000000000..94abb9d8b9
--- /dev/null
+++ b/app/lib/govuk_notify_personalisation/mmr_details_presenter.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+class GovukNotifyPersonalisation
+ class MmrDetailsPresenter
+ def initialize(personalisation)
+ @personalisation = personalisation
+ end
+
+ attr_reader :personalisation
+
+ delegate :academic_year,
+ :patient,
+ :programmes,
+ :short_patient_name,
+ :team,
+ to: :personalisation
+
+ def invitation_to_clinic_custom_mmr_message
+ return "" unless mmr_second_dose_required?
+
+ case team&.organisation&.ods_code
+ when "RT5" # Leicestershire Partnership Trust (LPT)
+ [
+ mmr_second_dose_waiting_period_message,
+ "It’s also possible for #{short_patient_name} to be vaccinated at your local GP surgery. " \
+ "To book an appointment, contact the surgery in the usual way."
+ ].join("\n\n")
+ when "RYG" # Coventry & Warwickshire Partnership NHS Trust (CWPT)
+ [
+ mmr_second_dose_waiting_period_message,
+ "## You have 2 options for booking the vaccination",
+ "You can ask your local GP surgery to give #{short_patient_name} their 2nd dose. " \
+ "To book an appointment, contact the surgery in the usual way."
+ ].join("\n\n")
+ end
+ end
+
+ def invitation_to_clinic_generic_message
+ [
+ (
+ if mmr_second_dose_required?
+ "If you would like your local GP surgery to give #{short_patient_name} " \
+ "their 2nd dose, contact the surgery in the usual way."
+ end
+ ),
+ "#{mmr_second_dose_required? ? "Alternatively, they" : "They"} can have this vaccination " \
+ "at a community clinic. If you’d like to book a clinic appointment, please contact " \
+ "us using the details below.",
+ (mmr_second_dose_waiting_period_message if mmr_second_dose_required?)
+ ].compact.join("\n\n")
+ end
+
+ def mmr_or_mmrv_vaccine
+ return if mmr_programme.blank?
+
+ if mmr_programme.variant_type == "mmrv"
+ "MMR or MMRV vaccine"
+ else
+ "MMR vaccine"
+ end
+ end
+
+ def mmr_programme
+ @mmr_programme ||= programmes.find(&:mmr?)
+ end
+
+ def mmr_second_dose_required?
+ mmr_programme.present? && patient_on_last_dose?
+ end
+
+ def next_mmr_dose_date
+ return if patient.nil?
+ return if mmr_programme.nil?
+
+ patient
+ .programme_status(mmr_programme, academic_year:)
+ .next_dose_eligible_date
+ &.to_fs(:long)
+ end
+
+ def patient_on_last_dose?
+ return unless patient
+ return if mmr_programme.nil?
+
+ patient
+ .reload
+ .programme_status(mmr_programme, academic_year:)
+ .on_last_dose?
+ end
+
+ private
+
+ def mmr_second_dose_waiting_period_message
+ "It’s important to wait at least 28 days after the 1st dose of an MMR or " \
+ "MMRV vaccination before getting the 2nd dose. #{short_patient_name} " \
+ "should not get the 2nd dose until #{next_mmr_dose_date}. Please keep this in " \
+ "mind when booking the appointment."
+ end
+ end
+end
diff --git a/app/lib/govuk_notify_personalisation/session_dates_presenter.rb b/app/lib/govuk_notify_personalisation/session_dates_presenter.rb
new file mode 100644
index 0000000000..122519400d
--- /dev/null
+++ b/app/lib/govuk_notify_personalisation/session_dates_presenter.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+class GovukNotifyPersonalisation
+ class SessionDatesPresenter
+ def initialize(personalisation)
+ @personalisation = personalisation
+ end
+
+ attr_reader :personalisation
+
+ delegate :consent_form, :session, to: :personalisation
+
+ def has_multiple_dates?
+ return false if session.nil?
+
+ session.future_dates.length > 1
+ end
+
+ def next_or_today_session_date
+ return "" unless session_dates_are_accurate?
+
+ session&.next_date(include_today: true)&.to_fs(:short_day_of_week)
+ end
+
+ def next_or_today_session_dates
+ return "" unless session_dates_are_accurate?
+
+ session
+ &.today_or_future_dates
+ &.map { it.to_fs(:short_day_of_week) }
+ &.to_sentence
+ end
+
+ def next_or_today_session_dates_or
+ return "" unless session_dates_are_accurate?
+
+ session
+ &.today_or_future_dates
+ &.map { it.to_fs(:short_day_of_week) }
+ &.to_sentence(last_word_connector: ", or ", two_words_connector: " or ")
+ end
+
+ def next_session_date
+ return "" unless session_dates_are_accurate?
+
+ session&.next_date(include_today: false)&.to_fs(:short_day_of_week)
+ end
+
+ def next_session_dates
+ return "" unless session_dates_are_accurate?
+
+ session&.future_dates&.map { it.to_fs(:short_day_of_week) }&.to_sentence
+ end
+
+ def next_session_dates_or
+ return "" unless session_dates_are_accurate?
+
+ session
+ &.future_dates
+ &.map { it.to_fs(:short_day_of_week) }
+ &.to_sentence(last_word_connector: ", or ", two_words_connector: " or ")
+ end
+
+ def subsequent_session_dates_offered_message
+ return nil if session.nil?
+
+ dates = session.future_dates.drop(1)
+ return "" if dates.empty?
+
+ "If they’re not seen, they’ll be offered the vaccination on #{
+ dates.map { it.to_fs(:short_day_of_week) }.to_sentence
+ }."
+ end
+
+ private
+
+ def session_dates_are_accurate?
+ consent_form ? consent_form.session_dates_are_accurate? : true
+ end
+ end
+end
diff --git a/app/lib/govuk_notify_personalisation/triage_details_presenter.rb b/app/lib/govuk_notify_personalisation/triage_details_presenter.rb
new file mode 100644
index 0000000000..10f3d6e73f
--- /dev/null
+++ b/app/lib/govuk_notify_personalisation/triage_details_presenter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class GovukNotifyPersonalisation
+ class TriageDetailsPresenter
+ def initialize(personalisation)
+ @personalisation = personalisation
+ end
+
+ attr_reader :personalisation
+
+ delegate :patient, :session, :short_patient_name, to: :personalisation
+
+ def delay_vaccination_review_context
+ return if patient.nil? || session.nil?
+
+ latest_delayed_triage =
+ patient.latest_delayed_triage(programme_types: session.programme_types)
+
+ return if latest_delayed_triage.nil?
+
+ session_date = session.next_date(include_today: true)
+ triage_date = latest_delayed_triage.created_at.to_date
+
+ if session_date && triage_date == session_date
+ "assessed #{short_patient_name} in the vaccination session"
+ else
+ "reviewed the answers you gave to the health questions about #{short_patient_name}"
+ end
+ end
+ end
+end
diff --git a/app/lib/govuk_notify_personalisation/vaccination_details_presenter.rb b/app/lib/govuk_notify_personalisation/vaccination_details_presenter.rb
new file mode 100644
index 0000000000..0391297365
--- /dev/null
+++ b/app/lib/govuk_notify_personalisation/vaccination_details_presenter.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+class GovukNotifyPersonalisation
+ class VaccinationDetailsPresenter
+ def initialize(personalisation)
+ @personalisation = personalisation
+ end
+
+ attr_reader :personalisation
+
+ delegate :academic_year,
+ :mmr_second_dose_required?,
+ :next_or_today_session_dates_or,
+ :patient,
+ :programmes,
+ :programme_name_for_parents,
+ :vaccination_record,
+ to: :personalisation
+
+ def is_catch_up?
+ return false if patient.nil? || programmes.empty?
+
+ @is_catch_up ||=
+ programmes.any? { it.is_catch_up?(year_group: patient_year_group) }
+ end
+
+ def outcome_not_administered?
+ vaccination_record.nil? || !outcome_administered?
+ end
+
+ def outcome_administered?
+ vaccination_record.nil? || vaccination_record.administered?
+ end
+
+ def reason_did_not_vaccinate
+ return if vaccination_record.nil? || vaccination_record.administered?
+
+ I18n.t(
+ vaccination_record.outcome,
+ scope: "mailers.vaccination_mailer.reasons_did_not_vaccinate",
+ short_patient_name:
+ )
+ end
+
+ def show_additional_instructions? =
+ vaccination_record.present? && !vaccination_record.already_had?
+
+ def vaccination
+ if vaccination_record.present?
+ # We're sending communication about a specific vaccination that took place.
+ "#{programme_names.to_sentence} vaccination".pluralize(
+ programme_names.length
+ )
+ else
+ # We're sending about a vaccination that will take place.
+ names = programme_names
+
+ if mmr_second_dose_required?
+ names = names.map { it == "MMR" ? "2nd dose of the MMR" : it }
+ end
+
+ "#{names.to_sentence} vaccination".pluralize(names.length)
+ end
+ end
+
+ def vaccination_and_dates
+ if next_or_today_session_dates_or.present?
+ "#{vaccination} on #{next_or_today_session_dates_or}"
+ else
+ vaccination
+ end
+ end
+
+ # TODO: Remove this method when schools start offering MMRV.
+ def vaccination_and_dates_sms
+ if next_or_today_session_dates_or.present?
+ "#{vaccination} on #{next_or_today_session_dates_or}"
+ else
+ vaccination
+ end
+ end
+
+ def vaccination_and_method
+ "#{programme_names_and_methods.to_sentence} vaccination".pluralize(
+ programme_names_and_methods.length
+ )
+ end
+
+ def vaccine
+ "#{programme_names.to_sentence} vaccine".pluralize(programme_names.length)
+ end
+
+ def vaccine_and_dose
+ if (dose_sequence = vaccination_record&.dose_sequence)
+ "#{programme_names.to_sentence} #{dose_sequence.ordinalize} dose"
+ else
+ programme_names.to_sentence
+ end
+ end
+
+ def vaccine_and_method
+ "#{programme_names_and_methods.to_sentence} vaccine".pluralize(
+ programme_names_and_methods.length
+ )
+ end
+
+ def vaccine_is?(method)
+ if vaccination_record
+ vaccination_record.vaccine&.method == method
+ elsif programmes.present?
+ if patient
+ programmes.any? do |programme|
+ patient.vaccine_criteria(
+ programme:,
+ academic_year:
+ ).primary_method == method
+ end
+ else
+ Vaccine.for_programmes(programmes).exists?(method:)
+ end
+ end
+ end
+
+ def vaccine_side_effects
+ side_effects =
+ if vaccination_record
+ vaccination_record.vaccine&.side_effects
+ elsif programmes.present?
+ if patient
+ programmes.flat_map do |programme|
+ patient.vaccine_criteria(programme:, academic_year:).side_effects
+ end
+ else
+ Vaccine.for_programmes(programmes).active.flat_map(&:side_effects)
+ end
+ end
+
+ return if side_effects.nil?
+
+ descriptions =
+ side_effects.map { Vaccine.human_enum_name(:side_effect, it) }.sort.uniq
+
+ descriptions.map { "- #{it}" }.join("\n")
+ end
+
+ private
+
+ def short_patient_name
+ personalisation.short_patient_name
+ end
+
+ def patient_year_group
+ @patient_year_group ||= patient.year_group(academic_year:)
+ end
+
+ def programme_names
+ @programme_names ||= programmes.map { programme_name_for_parents(it) }
+ end
+
+ def programme_names_and_methods
+ @programme_names_and_methods ||=
+ programmes.map do |programme|
+ if programme.has_multiple_vaccine_methods?
+ vaccine_method =
+ if vaccination_record
+ Vaccine.delivery_method_to_vaccine_method(
+ vaccination_record.delivery_method
+ )
+ elsif patient
+ patient.vaccine_criteria(
+ programme:,
+ academic_year:
+ ).primary_method
+ end
+
+ method_prefix =
+ Vaccine.human_enum_name(:method_prefix, vaccine_method)
+ "#{method_prefix} #{programme.name_in_sentence}".lstrip
+ else
+ programme.name_in_sentence
+ end
+ end
+ end
+ end
+end
diff --git a/app/lib/graph_records.rb b/app/lib/graph_records.rb
index 6b64f76482..7701335a81 100644
--- a/app/lib/graph_records.rb
+++ b/app/lib/graph_records.rb
@@ -61,30 +61,15 @@ class GraphRecords
ALLOWED_TYPES = DEFAULT_NODE_ORDER
DEFAULT_TRAVERSALS = {
- patient: {
- patient: %i[
- parents
- consents
- cohort_imports
- class_imports
- vaccination_records
- triages
- school
- patient_locations
- ],
- parent: %i[patients consents cohort_imports class_imports],
- consent: %i[consent_form patient parent],
- patient_location: %i[patient location],
- location: %i[sessions],
- session: %i[location programmes],
- vaccination_record: %i[session]
+ batch: {
+ batch: %i[team vaccine],
+ vaccine: %i[programme]
},
- parent: {
- parent: %i[class_imports cohort_imports consents patients],
- class_import: %i[session],
- consent: %i[parent patient],
- patient: %i[parents sessions consents],
- session: %i[location]
+ class_import: {
+ class_import: %i[team uploaded_by]
+ },
+ cohort_import: {
+ cohort_import: %i[team uploaded_by]
},
consent: {
consent: %i[consent_form parent patient programme],
@@ -92,158 +77,168 @@ class GraphRecords
patient: %i[parents]
},
consent_form: {
- consent_form: [:consents]
+ consent_form: %i[consents]
},
- vaccination_record: {
- vaccination_record: %i[
- patient
- programme
- session
- vaccine
- performed_by_user
- ],
- patient: [:consents],
- session: [:location],
- consent: [:programme]
+ gillick_assessment: {
+ gillick_assessment: %i[performed_by programme],
+ session: %i[location]
},
location: {
- location: %i[sessions team]
+ location: %i[sessions teams]
},
- session: {
- session: %i[location programmes session_dates]
- },
- session_attendance: {
- session_attendance: %i[session_date],
- session: %i[location],
- session_date: %i[session]
+ organisation: {
+ organisation: %i[teams]
},
- gillick_assessment: {
- gillick_assessment: %i[performed_by programme],
+ parent: {
+ class_import: %i[session],
+ consent: %i[parent patient],
+ parent: %i[class_imports cohort_imports consents patients],
+ patient: %i[consents parents sessions],
session: %i[location]
},
- triage: {
- triage: %i[patient performed_by programme]
+ patient: {
+ consent: %i[consent_form parent patient],
+ location: %i[sessions],
+ parent: %i[class_imports cohort_imports consents patients],
+ patient: %i[
+ class_imports
+ cohort_imports
+ consents
+ parents
+ patient_locations
+ school
+ triages
+ vaccination_records
+ ],
+ patient_location: %i[location patient],
+ session: %i[location programmes],
+ vaccination_record: %i[session]
+ },
+ patient_location: {
+ patient_location: %i[location patient]
},
programme: {
programme: %i[teams vaccines]
},
- organisation: {
- organisation: %i[teams]
+ session: {
+ session: %i[location programmes]
+ },
+ session_attendance: {
+ session: %i[location],
+ session_attendance: %i[session_date],
+ session_date: %i[session]
},
team: {
team: %i[organisation programmes]
},
- vaccine: {
- vaccine: %i[batches programme]
- },
- batch: {
- batch: %i[team vaccine],
- vaccine: [:programme]
+ triage: {
+ triage: %i[patient performed_by programme]
},
user: {
- user: %i[teams programmes],
- team: [:programmes]
- },
- session_date: {
- session_date: [:session],
- session: %i[location]
- },
- cohort_import: {
- cohort_import: %i[team uploaded_by]
+ team: %i[programmes],
+ user: %i[programmes teams]
},
- class_import: {
- class_import: %i[team uploaded_by]
+ vaccination_record: {
+ consent: %i[programme],
+ patient: %i[consents],
+ session: %i[location],
+ vaccination_record: %i[
+ patient
+ performed_by_user
+ programme
+ session
+ vaccine
+ ]
},
- patient_location: {
- patient_location: %i[patient location]
+ vaccine: {
+ vaccine: %i[batches programme]
}
}.freeze
DETAIL_WHITELIST = {
- consent: %i[
- response
- route
- created_at
- updated_at
- withdrawn_at
- invalidated_at
- ],
- session: %i[slug clinic? academic_year],
- session_attendance: %i[attending created_at updated_at],
- triage: %i[status created_at updated_at invalidated_at],
- vaccination_record: %i[
- outcome
- performed_at
- created_at
- updated_at
- discarded_at
- uuid
- ],
- programme: %i[type],
- vaccine: %i[upload_name],
- organisation: %i[ods_code],
- team: %i[name workgroup],
- subteam: %i[name],
- location: %i[name address_postcode type gias_year_groups],
- cohort_import: %i[
+ batch: %i[archived_at expiry number],
+ class_import: %i[
+ changed_record_count
csv_filename
+ exact_duplicate_record_count
+ new_record_count
processed_at
- status
rows_count
- new_record_count
- exact_duplicate_record_count
- changed_record_count
+ status
+ year_groups
],
- class_import: %i[
+ cohort_import: %i[
+ changed_record_count
csv_filename
+ exact_duplicate_record_count
+ new_record_count
processed_at
- status
rows_count
- new_record_count
- exact_duplicate_record_count
- changed_record_count
- year_groups
+ status
],
- session_date: %i[value],
- patient: %i[
- updated_from_pds_at
- date_of_death_recorded_at
- restricted_at
+ consent: %i[
+ created_at
invalidated_at
+ response
+ route
+ updated_at
+ withdrawn_at
],
- parent: %i[],
+ consent_form: %i[archived_at recorded_at response],
gillick_assessment: %i[
- knows_vaccination
- knows_disease
+ created_at
knows_consequences
knows_delivery
+ knows_disease
knows_side_effects
- created_at
+ knows_vaccination
],
- batch: %i[number expiry archived_at],
- user: %i[fallback_role uid],
- consent_form: %i[response recorded_at archived_at],
+ location: %i[address_postcode gias_year_groups name type],
+ organisation: %i[ods_code],
+ parent: %i[],
parent_relationship: %i[type],
- patient_location: %i[academic_year]
+ patient: %i[
+ date_of_death_recorded_at
+ invalidated_at
+ restricted_at
+ updated_from_pds_at
+ ],
+ patient_location: %i[academic_year],
+ programme: %i[type],
+ session: %i[academic_year clinic? dates slug],
+ session_attendance: %i[attending created_at updated_at],
+ subteam: %i[name],
+ team: %i[name workgroup],
+ triage: %i[created_at invalidated_at status updated_at],
+ user: %i[fallback_role uid],
+ vaccination_record: %i[
+ created_at
+ discarded_at
+ outcome
+ performed_at
+ updated_at
+ uuid
+ ],
+ vaccine: %i[upload_name]
}.freeze
EXTRA_DETAIL_WHITELIST_WITH_PII = {
+ consent_form: %i[address_postcode date_of_birth family_name given_name],
+ parent: %i[email full_name phone],
+ parent_relationship: %i[other_name],
patient: %i[
- nhs_number
- given_name
- family_name
- date_of_birth
address_line_1
address_line_2
- address_town
address_postcode
+ address_town
+ date_of_birth
date_of_death
+ family_name
+ given_name
+ nhs_number
pending_changes
],
- parent: %i[full_name email phone],
- user: %i[given_name family_name email fallback_role uid],
- consent_form: %i[given_name family_name address_postcode date_of_birth],
- parent_relationship: %i[other_name]
+ user: %i[email fallback_role family_name given_name uid]
}.freeze
DETAIL_WHITELIST_WITH_PII =
diff --git a/app/lib/mavis_cli/reports/export_automated_careplus.rb b/app/lib/mavis_cli/reports/export_automated_careplus.rb
index 80d592622b..91f6ba5a31 100644
--- a/app/lib/mavis_cli/reports/export_automated_careplus.rb
+++ b/app/lib/mavis_cli/reports/export_automated_careplus.rb
@@ -96,14 +96,64 @@ def call(
academic_year_value = academic_year&.to_i || AcademicYear.current
- csv =
- ::Reports::AutomatedCareplusExporter.call(
+ records =
+ ::Reports::AutomatedCareplusExporter.vaccination_records_scope(
team:,
academic_year: academic_year_value,
start_date: parsed_start_date,
end_date: parsed_end_date
)
+ programme_types =
+ records.unscope(:order).distinct.pluck(:programme_type)
+
+ csv =
+ ::Reports::AutomatedCareplusExporter.from_records(
+ team:,
+ academic_year: academic_year_value,
+ vaccination_records: records
+ )
+
+ if records.none?
+ puts "No records found. No CarePlus report was created."
+ return
+ end
+
+ now = Time.current
+
+ # we'll create the export with status "sent" for now
+ # in the future we'll change this to "pending" or
+ # prevent this tool from creating database entries at all
+
+ ActiveRecord::Base.transaction do
+ careplus_export =
+ CareplusExport.create!(
+ team:,
+ academic_year: academic_year_value,
+ date_from: parsed_start_date,
+ date_to: parsed_end_date,
+ programme_types:,
+ scheduled_at: now,
+ sent_at: now,
+ status: :sent,
+ csv_filename: File.basename(output),
+ csv_data: csv
+ )
+
+ now_iso = now.iso8601(6)
+ CareplusExportVaccinationRecord.insert_all!(
+ records.map do |record|
+ {
+ careplus_export_id: careplus_export.id,
+ vaccination_record_id: record.id,
+ change_type: 0,
+ created_at: now_iso,
+ updated_at: now_iso
+ }
+ end
+ )
+ end
+
File.write(output, csv)
puts "Exported to #{output}"
end
diff --git a/app/lib/mavis_cli/schools/add_programme_year_group.rb b/app/lib/mavis_cli/schools/add_programme_year_group.rb
index fe5a24f8d0..2222526bce 100644
--- a/app/lib/mavis_cli/schools/add_programme_year_group.rb
+++ b/app/lib/mavis_cli/schools/add_programme_year_group.rb
@@ -28,7 +28,7 @@ def call(urn_or_id:, programme_type:, year_groups:, id:, **)
if id
Location.find(urn_or_id)
else
- Location.school.find_by_urn_and_site(urn_or_id)
+ Location.gias_school.find_by_urn_and_site(urn_or_id)
end
if location.nil?
diff --git a/app/lib/mavis_cli/schools/add_to_team.rb b/app/lib/mavis_cli/schools/add_to_team.rb
index 7036e4a954..8100e4148f 100644
--- a/app/lib/mavis_cli/schools/add_to_team.rb
+++ b/app/lib/mavis_cli/schools/add_to_team.rb
@@ -83,7 +83,7 @@ def urns_are_valid?(urns, team, academic_year, schools)
valid = true
urns.each do |urn|
- location = Location.school.find_by_urn_and_site(urn)
+ location = Location.gias_school.find_by_urn_and_site(urn)
if location.nil?
warn "Could not find school with URN #{urn}."
@@ -93,7 +93,7 @@ def urns_are_valid?(urns, team, academic_year, schools)
schools << location
- all_site_locations = Location.school.where(urn: location.urn)
+ all_site_locations = Location.gias_school.where(urn: location.urn)
# Skip if no sites exist
next if all_site_locations.count == 1
diff --git a/app/lib/mavis_cli/schools/create.rb b/app/lib/mavis_cli/schools/create.rb
index 4d0f867128..3e669911b4 100644
--- a/app/lib/mavis_cli/schools/create.rb
+++ b/app/lib/mavis_cli/schools/create.rb
@@ -63,7 +63,7 @@ def call(
site:,
status:,
systm_one_code:,
- type: "school",
+ type: "gias_school",
url:,
urn:
)
diff --git a/app/lib/mavis_cli/schools/move_patients.rb b/app/lib/mavis_cli/schools/move_patients.rb
index d418356247..51424a9ce9 100644
--- a/app/lib/mavis_cli/schools/move_patients.rb
+++ b/app/lib/mavis_cli/schools/move_patients.rb
@@ -13,8 +13,8 @@ def call(source_urn:, target_urn:)
academic_year = AcademicYear.pending
- old_loc = Location.school.find_by_urn_and_site(source_urn)
- new_loc = Location.school.find_by_urn_and_site(target_urn)
+ old_loc = Location.gias_school.find_by_urn_and_site(source_urn)
+ new_loc = Location.gias_school.find_by_urn_and_site(target_urn)
if old_loc.nil? || new_loc.nil?
warn "Could not find one or both schools."
diff --git a/app/lib/mavis_cli/schools/remove_from_team.rb b/app/lib/mavis_cli/schools/remove_from_team.rb
index b0aa401dc4..63d7fdbd75 100644
--- a/app/lib/mavis_cli/schools/remove_from_team.rb
+++ b/app/lib/mavis_cli/schools/remove_from_team.rb
@@ -40,7 +40,7 @@ def call(team_workgroup:, subteam_name:, urns:, academic_year: nil, **)
locations =
urns.filter_map do |urn|
- location = Location.school.find_by_urn_and_site(urn)
+ location = Location.gias_school.find_by_urn_and_site(urn)
warn "Could not find school with URN #{urn}" if location.nil?
location
diff --git a/app/lib/mavis_cli/schools/remove_programme_year_group.rb b/app/lib/mavis_cli/schools/remove_programme_year_group.rb
index e1d1e2b060..d8d76149e9 100644
--- a/app/lib/mavis_cli/schools/remove_programme_year_group.rb
+++ b/app/lib/mavis_cli/schools/remove_programme_year_group.rb
@@ -17,7 +17,7 @@ class RemoveProgrammeYearGroup < Dry::CLI::Command
def call(urn:, programme_type:, year_groups:, **)
MavisCLI.load_rails
- location = Location.school.find_by_urn_and_site(urn)
+ location = Location.gias_school.find_by_urn_and_site(urn)
if location.nil?
warn "Could not find school."
diff --git a/app/lib/notifier/patient.rb b/app/lib/notifier/patient.rb
index 1f9a28a307..9eee4c152d 100644
--- a/app/lib/notifier/patient.rb
+++ b/app/lib/notifier/patient.rb
@@ -245,7 +245,7 @@ def generate_consent_templates(
outbreak:,
type:
)
- is_school = location.school?
+ is_school = location.gias_school?
base_template = :"consent_#{is_school ? "school" : "clinic"}_#{type}"
# We can only handle a single programme group or variant in the template.
diff --git a/app/lib/notifier/vaccination_record.rb b/app/lib/notifier/vaccination_record.rb
index 79db57976f..5350ec9740 100644
--- a/app/lib/notifier/vaccination_record.rb
+++ b/app/lib/notifier/vaccination_record.rb
@@ -15,17 +15,10 @@ def send_confirmation(sent_by:)
:vaccination_not_administered
end
- email_template_name =
- if vaccination_record.administered?
- :"#{template_name}_#{vaccination_record.programme_type}"
- else
- template_name
- end
-
parents.each do |parent|
params = { parent:, vaccination_record:, sent_by: }
- EmailDeliveryJob.perform_later(email_template_name, **params)
+ EmailDeliveryJob.perform_later(template_name, **params)
if parent.phone_receive_updates
SMSDeliveryJob.perform_later(template_name, **params)
diff --git a/app/lib/notify_template.rb b/app/lib/notify_template.rb
index fa421f2d43..61ef2e6887 100644
--- a/app/lib/notify_template.rb
+++ b/app/lib/notify_template.rb
@@ -60,7 +60,7 @@ def initialize(name:, channel:, content:)
def render(personalisation)
ctx = personalisation.instance_eval { binding }
- body = ERB.new(@body, trim_mode: nil).result(ctx)
+ body = ERB.new(@body, trim_mode: "-").result(ctx)
return { body: } if @channel == :sms
{ subject: ERB.new(@subject, trim_mode: nil).result(ctx), body: }
diff --git a/app/lib/patient_status_updater.rb b/app/lib/patient_status_updater.rb
index aa460848a1..2ea8d83efb 100644
--- a/app/lib/patient_status_updater.rb
+++ b/app/lib/patient_status_updater.rb
@@ -85,7 +85,7 @@ def update_registration_statuses!
.joins(session: :team_location)
.where(team_location: { academic_year: academic_years })
.includes(:attendance_records, :patient, :session, :vaccination_records)
- .find_in_batches(batch_size: 10_000) do |batch|
+ .find_in_batches do |batch|
batch.each(&:assign_status)
Patient::RegistrationStatus.import!(
diff --git a/app/lib/reports/automated_careplus_exporter.rb b/app/lib/reports/automated_careplus_exporter.rb
index fa873d1019..4f82acc0de 100644
--- a/app/lib/reports/automated_careplus_exporter.rb
+++ b/app/lib/reports/automated_careplus_exporter.rb
@@ -1,25 +1,61 @@
# frozen_string_literal: true
class Reports::AutomatedCareplusExporter
+ VACCINE_COLUMNS = %i[
+ vaccine
+ dose
+ reason_not_given
+ site
+ manufacturer
+ batch_number
+ ].freeze
+
def self.call(team:, academic_year:, start_date:, end_date:)
Reports::CareplusExporter.call(
+ **shared_args(team:, academic_year:),
+ start_date:,
+ end_date:,
+ include_missing_nhs_number: false
+ )
+ end
+
+ def self.from_records(vaccination_records:, team:, academic_year:)
+ Reports::CareplusExporter.from_records(
+ **shared_args(team:, academic_year:),
+ vaccination_records:
+ vaccination_records.includes(
+ :patient,
+ :vaccine,
+ session: %i[location team_location]
+ )
+ )
+ end
+
+ def self.vaccination_records_scope(
+ team:,
+ academic_year:,
+ start_date:,
+ end_date:
+ )
+ Reports::CareplusExporter.vaccination_records_scope(
team:,
programmes: team.programmes,
academic_year:,
start_date:,
end_date:,
- include_gender: false,
- include_missing_nhs_number: false,
- vaccine_columns: %i[
- vaccine
- dose
- reason_not_given
- site
- manufacturer
- batch_number
- ]
+ include_missing_nhs_number: false
)
end
- private_class_method :new
+ def self.shared_args(team:, academic_year:)
+ {
+ team:,
+ programmes: team.programmes,
+ academic_year:,
+ include_gender: false,
+ vaccine_columns: VACCINE_COLUMNS
+ }
+ end
+
+ private_class_method :new, :shared_args
end
diff --git a/app/lib/reports/careplus_exporter.rb b/app/lib/reports/careplus_exporter.rb
index 9e6da8d55c..3e7a4f49f7 100644
--- a/app/lib/reports/careplus_exporter.rb
+++ b/app/lib/reports/careplus_exporter.rb
@@ -30,19 +30,15 @@ def initialize(
team:,
programmes:,
academic_year:,
- start_date:,
- end_date:,
+ vaccination_records:,
include_gender:,
- include_missing_nhs_number:,
vaccine_columns:
)
@team = team
@programmes = programmes
@academic_year = academic_year
- @start_date = start_date
- @end_date = end_date
+ @vaccination_records = vaccination_records
@include_gender = include_gender
- @include_missing_nhs_number = include_missing_nhs_number
@vaccine_columns = vaccine_columns
end
@@ -57,7 +53,79 @@ def call
end
end
- def self.call(...) = new(...).call
+ def self.call(
+ team:,
+ programmes:,
+ academic_year:,
+ start_date:,
+ end_date:,
+ include_gender:,
+ include_missing_nhs_number:,
+ vaccine_columns:
+ )
+ vaccination_records =
+ vaccination_records_scope(
+ team:,
+ programmes:,
+ academic_year:,
+ start_date:,
+ end_date:,
+ include_missing_nhs_number:
+ ).includes(:patient, :vaccine, session: %i[location team_location])
+
+ from_records(
+ vaccination_records:,
+ team:,
+ programmes:,
+ academic_year:,
+ include_gender:,
+ vaccine_columns:
+ )
+ end
+
+ def self.from_records(
+ vaccination_records:,
+ team:,
+ programmes:,
+ academic_year:,
+ include_gender:,
+ vaccine_columns:
+ )
+ new(
+ vaccination_records:,
+ team:,
+ programmes:,
+ academic_year:,
+ include_gender:,
+ vaccine_columns:
+ ).call
+ end
+
+ def self.vaccination_records_scope(
+ team:,
+ programmes:,
+ academic_year:,
+ start_date:,
+ end_date:,
+ include_missing_nhs_number:
+ )
+ scope =
+ team
+ .vaccination_records
+ .sourced_from_service
+ .for_programmes(programmes)
+ .for_academic_year(academic_year)
+ .administered
+ .order_by_performed_at
+ .created_or_updated_between(start_date, end_date)
+
+ scope =
+ scope.joins(:patient).merge(
+ Patient.with_nhs_number
+ ) unless include_missing_nhs_number
+
+ scope
+ end
private_class_method :new
@@ -73,10 +141,8 @@ def self.call(...) = new(...).call
attr_reader :team,
:programmes,
:academic_year,
- :start_date,
- :end_date,
+ :vaccination_records,
:include_gender,
- :include_missing_nhs_number,
:vaccine_columns
def headers
@@ -120,52 +186,6 @@ def gender_row_value(patient)
include_gender ? [GENDER_CODE_MAPPINGS[patient.gender_code]] : []
end
- def vaccination_records
- scope =
- VaccinationRecord
- .kept
- .sourced_from_service
- .for_programmes(programmes)
- .where(team_location: { team_id: team.id })
- .for_academic_year(academic_year)
- .administered
- .order_by_performed_at
- .includes(:patient, :vaccine, session: %i[location team_location])
-
- if start_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at >= ?",
- start_date.beginning_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at >= ?",
- start_date.beginning_of_day
- )
- )
- end
-
- if end_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at <= ?",
- end_date.end_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at <= ?",
- end_date.end_of_day
- )
- )
- end
-
- scope =
- scope.joins(:patient).merge(
- Patient.with_nhs_number
- ) unless include_missing_nhs_number
-
- scope
- end
-
def consents
@consents ||=
Consent
@@ -199,7 +219,7 @@ def rows(patient:, vaccination_records:)
"",
date.strftime("%d/%m/%Y"),
records.first.performed_at.strftime("%H:%M"),
- session.location.school? ? "SC" : "CL", # Venue Type
+ session.location.gias_school? ? "SC" : "CL", # Venue Type
session.location.dfe_number || team.careplus_venue_code, # Venue Code
team.careplus_staff_type,
team.careplus_staff_code,
diff --git a/app/lib/reports/export_formatters.rb b/app/lib/reports/export_formatters.rb
index 604f2c7198..e09b70bafe 100644
--- a/app/lib/reports/export_formatters.rb
+++ b/app/lib/reports/export_formatters.rb
@@ -4,7 +4,7 @@ module Reports::ExportFormatters
extend ActiveSupport::Concern
def school_urn(location:, patient:)
- if location&.school?
+ if location&.gias_school?
location.urn
elsif (school = patient.school)
school.urn
@@ -14,11 +14,11 @@ def school_urn(location:, patient:)
end
def school_name(location:, patient:)
- location&.school? ? location.name : patient.school&.name || ""
+ location&.gias_school? ? location.name : patient.school&.name || ""
end
def care_setting(location:)
- if location&.school?
+ if location&.gias_school?
ImmunisationImportRow::CARE_SETTING_SCHOOL
else
ImmunisationImportRow::CARE_SETTING_COMMUNITY
@@ -26,7 +26,7 @@ def care_setting(location:)
end
def clinic_name(location:, vaccination_record:)
- location&.school? ? "" : vaccination_record.location_name
+ location&.gias_school? ? "" : vaccination_record.location_name
end
def consent_status(patient:, programme:, academic_year:)
diff --git a/app/lib/reports/programme_vaccinations_exporter.rb b/app/lib/reports/programme_vaccinations_exporter.rb
index 80ae2fda3a..925dc0b860 100644
--- a/app/lib/reports/programme_vaccinations_exporter.rb
+++ b/app/lib/reports/programme_vaccinations_exporter.rb
@@ -88,48 +88,20 @@ def headers
end
def vaccination_records
- scope =
- team
- .vaccination_records
- .sourced_from_service
- .for_programme(programme)
- .for_academic_year(academic_year)
- .includes(
- :location,
- :performed_by_user,
- :session,
- :supplied_by,
- :vaccine,
- patient: %i[programme_statuses gp_practice school]
- )
-
- if start_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at >= ?",
- start_date.beginning_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at >= ?",
- start_date.beginning_of_day
- )
- )
- end
-
- if end_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at <= ?",
- end_date.end_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at <= ?",
- end_date.end_of_day
- )
- )
- end
-
- scope
+ team
+ .vaccination_records
+ .sourced_from_service
+ .for_programme(programme)
+ .for_academic_year(academic_year)
+ .created_or_updated_between(start_date, end_date)
+ .includes(
+ :location,
+ :performed_by_user,
+ :session,
+ :supplied_by,
+ :vaccine,
+ patient: %i[programme_statuses gp_practice school]
+ )
end
def consents
diff --git a/app/lib/reports/school_moves_exporter.rb b/app/lib/reports/school_moves_exporter.rb
index 9ab1826f88..c72bf4ecab 100644
--- a/app/lib/reports/school_moves_exporter.rb
+++ b/app/lib/reports/school_moves_exporter.rb
@@ -54,14 +54,14 @@ def school_move_log_entries
.where(
SchoolMoveLogEntry
.where("patient_id = patients.id")
- .where(school: team.schools)
+ .where(school: team.gias_schools)
.arel
.exists
)
scope =
SchoolMoveLogEntry
- .where(school: team.schools)
+ .where(school: team.gias_schools)
.or(SchoolMoveLogEntry.where(patient: team.patients, school: nil))
.or(
SchoolMoveLogEntry
@@ -94,7 +94,7 @@ def row(log_entry)
location = log_entry.school
systm_one_school_code =
- if location&.school?
+ if location&.gias_school?
location.systm_one_code
else
patient&.school&.systm_one_code
diff --git a/app/lib/reports/systm_one_exporter.rb b/app/lib/reports/systm_one_exporter.rb
index 896a64d335..1a712d197f 100644
--- a/app/lib/reports/systm_one_exporter.rb
+++ b/app/lib/reports/systm_one_exporter.rb
@@ -136,49 +136,21 @@ def headers
end
def vaccination_records
- scope =
- team
- .vaccination_records
- .administered
- .sourced_from_service
- .for_programme(programme)
- .for_academic_year(academic_year)
- .includes(
- :location,
- :patient,
- :performed_by_user,
- :session,
- :supplied_by,
- :vaccine
- )
-
- if start_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at >= ?",
- start_date.beginning_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at >= ?",
- start_date.beginning_of_day
- )
- )
- end
-
- if end_date.present?
- scope =
- scope.where(
- "vaccination_records.created_at <= ?",
- end_date.end_of_day
- ).or(
- scope.where(
- "vaccination_records.updated_at <= ?",
- end_date.end_of_day
- )
- )
- end
-
- scope
+ team
+ .vaccination_records
+ .administered
+ .sourced_from_service
+ .for_programme(programme)
+ .for_academic_year(academic_year)
+ .created_or_updated_between(start_date, end_date)
+ .includes(
+ :location,
+ :patient,
+ :performed_by_user,
+ :session,
+ :supplied_by,
+ :vaccine
+ )
end
def row(vaccination_record:)
@@ -214,7 +186,7 @@ def practice_code(vaccination_record)
if location&.systm_one_code.present?
location.systm_one_code
- elsif location&.school?
+ elsif location&.gias_school?
location.urn
elsif location
location.ods_code
diff --git a/app/lib/stats/organisations.rb b/app/lib/stats/organisations.rb
index c5e442aceb..4b1cc09082 100644
--- a/app/lib/stats/organisations.rb
+++ b/app/lib/stats/organisations.rb
@@ -60,8 +60,8 @@ def calculate_cohort_total(programme)
def calculate_school_total(programme)
Team
.where(id: teams.pluck(:id))
- .includes(:schools)
- .flat_map(&:schools)
+ .includes(:gias_schools)
+ .flat_map(&:gias_schools)
.uniq
.count { |location| location.programmes.include?(programme) }
end
diff --git a/app/lib/timeline_records.rb b/app/lib/timeline_records.rb
index 79627a34ed..8ef975068f 100644
--- a/app/lib/timeline_records.rb
+++ b/app/lib/timeline_records.rb
@@ -5,7 +5,7 @@ class TimelineRecords
changesets: %i[import_id import_type],
class_imports: %i[],
cohort_imports: %i[],
- consents: %i[response route],
+ consents: %i[programme_type response route],
school_move_log_entries: %i[school_id user_id],
school_moves: %i[school_id source],
sessions: %i[location_id],
@@ -35,10 +35,17 @@ class TimelineRecords
rows_count
status
],
- consents: %i[invalidated_at response route updated_at withdrawn_at],
+ consents: %i[
+ invalidated_at
+ programme_type
+ response
+ route
+ updated_at
+ withdrawn_at
+ ],
gillick_assessments: %i[],
- parent_relationships: %i[],
- parents: %i[],
+ notify_log_entries: %i[purpose],
+ patient_locations: %i[academic_year date_range location_id],
pds_search_results: %i[step],
school_move_log_entries: %i[school_id user_id],
school_moves: %i[school_id source],
@@ -136,12 +143,12 @@ class TimelineRecords
ALLOWED_AUDITED_CHANGES_WITH_PII =
(ALLOWED_AUDITED_CHANGES + ALLOWED_AUDITED_CHANGES_PII).uniq.freeze
- def initialize(patient, detail_config: {}, audit_config: {}, show_pii: false)
+ def initialize(patient, details_config: {}, audit_config: {}, show_pii: false)
@patient = patient
@patient_id = @patient.id
@patient_events = patient_events(@patient)
@additional_events = additional_events(@patient)
- @detail_config = extract_detail_config(detail_config)
+ @detail_config = extract_detail_config(details_config)
@events = []
@audit_config = audit_config
@show_pii = show_pii
@@ -188,7 +195,7 @@ def extract_detail_config(detail_config)
end
def details
- @details ||= DEFAULT_DETAILS_CONFIG.merge(@detail_config)
+ @detail_config
end
def audits
diff --git a/app/lib/training_onboarding_configuration.rb b/app/lib/training_onboarding_configuration.rb
index ddb7e03a20..2a22cb18a6 100644
--- a/app/lib/training_onboarding_configuration.rb
+++ b/app/lib/training_onboarding_configuration.rb
@@ -87,7 +87,7 @@ def users
def schools
scope =
Location
- .school
+ .gias_school
.open
.without_team(academic_year:)
.order("RANDOM()")
diff --git a/app/models/cis2_info.rb b/app/models/cis2_info.rb
index d5b888abfb..17e60d09a0 100644
--- a/app/models/cis2_info.rb
+++ b/app/models/cis2_info.rb
@@ -98,8 +98,7 @@ def is_support_without_pii_access?
end
def is_support_with_pii_access?
- role_code == SUPPORT_ROLE && can_access_sensitive_flagged_records? &&
- can_view_detailed_health_records?
+ role_code == SUPPORT_ROLE && can_view_detailed_health_records?
end
private
diff --git a/app/models/cohort_import_row.rb b/app/models/cohort_import_row.rb
index 95de2138ba..7f50efcb83 100644
--- a/app/models/cohort_import_row.rb
+++ b/app/models/cohort_import_row.rb
@@ -23,7 +23,7 @@ def school
def schools
Location
- .where(type: %w[school generic_school])
+ .school
.joins(:team_locations)
.where(team_locations: { team:, academic_year: })
end
diff --git a/app/models/concerns/has_many_team_locations.rb b/app/models/concerns/has_many_team_locations.rb
index 853b4dd6e4..d82dbf4b4c 100644
--- a/app/models/concerns/has_many_team_locations.rb
+++ b/app/models/concerns/has_many_team_locations.rb
@@ -23,8 +23,8 @@ module HasManyTeamLocations
through: :team_locations,
source: :location
- has_many :schools,
- -> { distinct.school },
+ has_many :gias_schools,
+ -> { distinct.gias_school },
through: :team_locations,
source: :location
end
diff --git a/app/models/concerns/refusable.rb b/app/models/concerns/refusable.rb
index 992d86e435..a845ec87fb 100644
--- a/app/models/concerns/refusable.rb
+++ b/app/models/concerns/refusable.rb
@@ -20,7 +20,8 @@ module Refusable
will_be_vaccinated_elsewhere: 2,
medical_reasons: 3,
personal_choice: 4,
- other: 5
+ other: 5,
+ do_not_want_vaccination_at_school: 6
},
prefix: true,
validate: {
diff --git a/app/models/consent_form.rb b/app/models/consent_form.rb
index 2b214742bc..c80cc4069b 100644
--- a/app/models/consent_form.rb
+++ b/app/models/consent_form.rb
@@ -133,7 +133,7 @@ class ConsentForm < ApplicationRecord
source: :team_locations
has_many :eligible_schools,
- -> { school },
+ -> { gias_school },
through: :eligible_team_locations,
source: :location
@@ -329,6 +329,7 @@ class ConsentForm < ApplicationRecord
ETHNICITY_STEPS = %i[ethnicity ethnic_group ethnic_background].freeze
FOLLOW_UP_REQUIRED_REASONS = %w[
contains_gelatine
+ do_not_want_vaccination_at_school
medical_reasons
personal_choice
other
@@ -784,7 +785,7 @@ def health_answers_valid?
true
end
- def location_is_school? = location.school?
+ def location_is_school? = location.gias_school?
def location_is_clinic? = location.generic_clinic? || location.generic_school?
diff --git a/app/models/draft_session.rb b/app/models/draft_session.rb
index a9c74748af..611d6fd2ff 100644
--- a/app/models/draft_session.rb
+++ b/app/models/draft_session.rb
@@ -128,7 +128,7 @@ def team_location
delegate :id, to: :team_location, prefix: true
- def school? = location&.school?
+ def school? = location&.gias_school?
def generic_clinic? = location&.generic_clinic?
@@ -329,7 +329,7 @@ def valid_school_ids
LocationPolicy::Scope
.new(@current_user, Location)
.resolve
- .school
+ .gias_school
.joins(:team_locations)
.where(team_locations: { team:, academic_year: })
.pluck(:"locations.id")
diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb
index 4ebefa1571..6cfd257d57 100644
--- a/app/models/draft_vaccination_record.rb
+++ b/app/models/draft_vaccination_record.rb
@@ -307,7 +307,7 @@ def batch_expiry
def location_is_school
return if location_id.blank?
- unless location&.school?
+ unless location&.gias_school?
errors.add(:location_id, "The location must be a school")
end
end
diff --git a/app/models/immunisation_import.rb b/app/models/immunisation_import.rb
index facf07934a..d4fac6250e 100644
--- a/app/models/immunisation_import.rb
+++ b/app/models/immunisation_import.rb
@@ -211,7 +211,7 @@ def postprocess_rows!
end
PatientTeamUpdater.call(patient_scope: patients)
- PatientStatusUpdater.call(patient_scope: patients)
+ PatientStatusUpdater.call(patient_scope: Patient.where(id: patients.ids))
vaccination_records
.includes(:patient, :team, :subteam)
diff --git a/app/models/immunisation_import_row.rb b/app/models/immunisation_import_row.rb
index d0b69051b0..315b55b6df 100644
--- a/app/models/immunisation_import_row.rb
+++ b/app/models/immunisation_import_row.rb
@@ -352,7 +352,7 @@ def location_name
return unless location.nil?
if is_school_setting? || (is_unknown_setting? && clinic_name.blank?)
- school&.school? ? school.name : school_name&.to_s || "Unknown"
+ school&.gias_school? ? school.name : school_name&.to_s || "Unknown"
else
clinic&.name || clinic_name&.to_s || "Unknown"
end
@@ -383,8 +383,8 @@ def school
elsif school_urn.to_s == Location::URN_UNKNOWN
team.unknown_school
elsif school_urn.present?
- Location.school.find_by_urn_and_site(school_urn.to_s) ||
- Location.school.find_by(systm_one_code: school_urn.to_s)
+ Location.gias_school.find_by_urn_and_site(school_urn.to_s) ||
+ Location.gias_school.find_by(systm_one_code: school_urn.to_s)
end
end
@@ -672,7 +672,7 @@ def validate_batch_expiry
"must be less than 15 years in the future"
)
elsif date < EARLIEST_BATCH_EXPIRY
- errors.add(batch_expiry.header, "must be more than 15 years old")
+ errors.add(batch_expiry.header, "must be less than 15 years old")
end
else
errors.add(batch_expiry.header, "Enter a date in the correct format.")
@@ -1173,8 +1173,8 @@ def validate_school_urn
school_urn_acceptable =
school_urn.to_s.in?(
[Location::URN_HOME_EDUCATED, Location::URN_UNKNOWN]
- ) || Location.school.where_urn_and_site(school_urn.to_s).exists? ||
- Location.school.exists?(systm_one_code: school_urn.to_s)
+ ) || Location.gias_school.where_urn_and_site(school_urn.to_s).exists? ||
+ Location.gias_school.exists?(systm_one_code: school_urn.to_s)
unless school_urn_acceptable
errors.add(
diff --git a/app/models/local_authority.rb b/app/models/local_authority.rb
index ba2c250b45..337df18071 100644
--- a/app/models/local_authority.rb
+++ b/app/models/local_authority.rb
@@ -25,6 +25,7 @@
# index_local_authorities_on_nation_and_short_name (nation,short_name)
# index_local_authorities_on_short_name (short_name)
#
+
class LocalAuthority < ApplicationRecord
self.primary_key = :mhclg_code
@@ -32,10 +33,7 @@ class LocalAuthority < ApplicationRecord
validates :gias_code, uniqueness: true, allow_nil: true
validates :gss_code, uniqueness: true, allow_nil: true
- has_many :postcodes,
- foreign_key: :gss_code,
- primary_key: :gss_code,
- class_name: "LocalAuthority::Postcode"
+ has_many :postcodes, foreign_key: :gss_code, primary_key: :gss_code
enum :nation,
{
@@ -80,8 +78,6 @@ def self.from_my_society_import_row(data)
end
def self.for_postcode(postcode)
- joins(:postcodes).merge(
- LocalAuthority::Postcode.where(value: postcode)
- ).first
+ joins(:postcodes).merge(Postcode.where(value: postcode)).first
end
end
diff --git a/app/models/local_authority/postcode.rb b/app/models/local_authority/postcode.rb
index fabfaaff8b..81087af14b 100644
--- a/app/models/local_authority/postcode.rb
+++ b/app/models/local_authority/postcode.rb
@@ -14,11 +14,9 @@
# index_local_authority_postcodes_on_gss_code (gss_code)
# index_local_authority_postcodes_on_value (value) UNIQUE
#
+
class LocalAuthority::Postcode < ApplicationRecord
- belongs_to :local_authority,
- foreign_key: :gss_code,
- primary_key: :gss_code,
- optional: true
+ belongs_to :local_authority, foreign_key: :gss_code, primary_key: :gss_code
normalizes :value,
with: ->(given_value) do
diff --git a/app/models/location.rb b/app/models/location.rb
index a378a32dcb..ba629a099b 100644
--- a/app/models/location.rb
+++ b/app/models/location.rb
@@ -31,6 +31,7 @@
# index_locations_on_urn (urn) UNIQUE WHERE ((type = 0) AND (site IS NULL))
# index_locations_on_urn_and_site (urn,site) UNIQUE
#
+
class Location < ApplicationRecord
self.inheritance_column = nil
@@ -88,7 +89,7 @@ class Location < ApplicationRecord
enum :type,
{
- school: 0,
+ gias_school: 0,
generic_clinic: 1,
community_clinic: 2,
gp_practice: 3,
@@ -97,6 +98,8 @@ class Location < ApplicationRecord
scope :clinic, -> { generic_clinic.or(community_clinic) }
+ scope :school, -> { gias_school.or(generic_school) }
+
scope :where_urn_and_site,
->(urn_and_site) do
where(
@@ -181,6 +184,15 @@ class Location < ApplicationRecord
validates :urn, inclusion: [URN_HOME_EDUCATED, URN_UNKNOWN]
end
+ with_options if: :gias_school? do
+ validates :gias_establishment_number, presence: true
+ validates :gias_local_authority_code, presence: true
+ validates :gias_phase, inclusion: Location.gias_phases.keys
+ validates :ods_code, absence: true
+ validates :site, uniqueness: { scope: :urn }, allow_nil: true
+ validates :urn, presence: true, uniqueness: { unless: :site }
+ end
+
with_options if: :gp_practice? do
validates :gias_establishment_number, absence: true
validates :gias_local_authority_code, absence: true
@@ -190,15 +202,6 @@ class Location < ApplicationRecord
validates :urn, absence: true
end
- with_options if: :school? do
- validates :gias_establishment_number, presence: true
- validates :gias_local_authority_code, presence: true
- validates :gias_phase, inclusion: Location.gias_phases.keys
- validates :ods_code, absence: true
- validates :site, uniqueness: { scope: :urn }, allow_nil: true
- validates :urn, presence: true, uniqueness: { unless: :site }
- end
-
delegate :fhir_reference, to: :fhir_mapper
def self.find_by_urn_and_site(urn_and_site)
@@ -229,8 +232,10 @@ def programmes
def clinic? = generic_clinic? || community_clinic?
+ def school? = gias_school? || generic_school?
+
def dfe_number
- "#{gias_local_authority_code}#{gias_establishment_number}" if school?
+ "#{gias_local_authority_code}#{gias_establishment_number}" if gias_school?
end
def phase
diff --git a/app/models/notify_log_entry.rb b/app/models/notify_log_entry.rb
index 12a55bc4d2..eb1bd91fd2 100644
--- a/app/models/notify_log_entry.rb
+++ b/app/models/notify_log_entry.rb
@@ -72,7 +72,12 @@ class NotifyLogEntry < ApplicationRecord
"fa3c8dd5-4688-4b93-960a-1d422c4e5597" => :triage_vaccination_will_happen,
"6e4c514d-fcc9-4bc8-b7eb-e222a1445681" => :session_school_reminder,
"604ee667-c996-471e-b986-79ab98d0767c" => :consent_confirmation_triage,
- "f2921e23-4b73-4e44-abbb-38b0e235db8e" => :consent_confirmation_clinic
+ "f2921e23-4b73-4e44-abbb-38b0e235db8e" => :consent_confirmation_clinic,
+ "8a65d7b5-045c-4f26-8f76-6e593c14cb6d" => :vaccination_administered_hpv,
+ "38727494-9a81-42b3-9c1f-5c31e55333e7" => :vaccination_administered_menacwy,
+ "3abe7ca8-a889-484b-ab9f-07523302eb6a" => :vaccination_administered_td_ipv,
+ "7238ee27-5840-40e5-b9b9-3130ba4cd4fa" => :vaccination_administered_flu,
+ "0b1095db-fb38-4105-9f01-a364fa8bbb1c" => :vaccination_administered_mmr
}.freeze
self.inheritance_column = nil
diff --git a/app/models/onboarding.rb b/app/models/onboarding.rb
index af9b4649ba..b9f6c2346f 100644
--- a/app/models/onboarding.rb
+++ b/app/models/onboarding.rb
@@ -180,7 +180,8 @@ def no_schools_with_existing_team_attachments
urn = school.urn
next if urn.blank?
- locations_with_teams = Location.school.where(urn:).joins(:teams).distinct
+ locations_with_teams =
+ Location.gias_school.where(urn:).joins(:teams).distinct
next unless locations_with_teams.exists?
@@ -304,7 +305,7 @@ class ExistingSchool
validates :status, inclusion: %w[open opening]
def location
- @location ||= Location.school.find_by_urn_and_site(urn)
+ @location ||= Location.gias_school.find_by_urn_and_site(urn)
end
delegate :status, to: :location, allow_nil: true
@@ -344,7 +345,7 @@ class NewSchoolSite
validates :site, presence: true
def original_location
- @original_location ||= Location.school.find_by_urn_and_site(urn)
+ @original_location ||= Location.gias_school.find_by_urn_and_site(urn)
end
def location
diff --git a/app/models/patient.rb b/app/models/patient.rb
index 4564a5defb..396918584e 100644
--- a/app/models/patient.rb
+++ b/app/models/patient.rb
@@ -811,7 +811,7 @@ def locations_are_correct_type
errors.add(:gp_practice, "must be a GP practice location type")
end
- if school && !(school.school? || school.generic_school?)
+ if school && !school.school?
errors.add(:school, "must be a school location type")
end
end
diff --git a/app/models/patient/programme_status.rb b/app/models/patient/programme_status.rb
index f0bbb68d33..9009503317 100644
--- a/app/models/patient/programme_status.rb
+++ b/app/models/patient/programme_status.rb
@@ -172,6 +172,11 @@ def assign
def vaccine_criteria = VaccineCriteria.from_programme_status(self)
+ def needs_more_doses?
+ dose_sequence.present? &&
+ dose_sequence <= Programme.find(programme_type).maximum_dose_sequence
+ end
+
def on_last_dose?
dose_sequence == Programme.find(programme_type).maximum_dose_sequence
end
diff --git a/app/models/programme.rb b/app/models/programme.rb
index 12e1029e87..6418700e5b 100644
--- a/app/models/programme.rb
+++ b/app/models/programme.rb
@@ -26,6 +26,14 @@ class InvalidType < StandardError
"td_ipv" => (9..11).to_a
}.freeze
+ DEFAULT_YEAR_GROUPS_WITH_OVER_16S_BY_TYPE = {
+ "flu" => (0..11).to_a,
+ "hpv" => (8..13).to_a,
+ "menacwy" => (9..13).to_a,
+ "mmr" => (0..13).to_a,
+ "td_ipv" => (9..13).to_a
+ }.freeze
+
MAXIMUM_DOSE_SEQUENCES = {
"flu" => 2,
"hpv" => 3,
@@ -155,6 +163,11 @@ def name
@name ||= I18n.t(translation_key, scope: :programme_types)
end
+ def name_on_nhs_uk
+ @name_on_nhs_uk ||=
+ I18n.t(type, scope: :programme_names_on_nhs_uk, default: nil)
+ end
+
def name_in_sentence
@name_in_sentence = flu? ? name.downcase : name
end
@@ -204,7 +217,13 @@ def can_save_to_todays_batch? = !mmr?
def triage_on_vaccination_history? = menacwy? || td_ipv?
- def default_year_groups = DEFAULT_YEAR_GROUPS_BY_TYPE.fetch(type)
+ def default_year_groups
+ if Flipper.enabled?(:vaccinating_16_plus_year_olds)
+ DEFAULT_YEAR_GROUPS_WITH_OVER_16S_BY_TYPE.fetch(type)
+ else
+ DEFAULT_YEAR_GROUPS_BY_TYPE.fetch(type)
+ end
+ end
def is_catch_up?(year_group:)
return nil if seasonal?
@@ -242,6 +261,10 @@ def maximum_dose_sequence = MAXIMUM_DOSE_SEQUENCES.fetch(type)
def minimum_dose_interval = MINIMUM_DOSE_INTERVALS.fetch(type)
+ # TODO: Make this more explicit when other programmes start
+ # administering multiple doses (e.g. immunocompromised schedules).
+ def multi_dose? = minimum_dose_interval.present?
+
def import_names = IMPORT_NAMES.fetch(type)
def snomed_target_disease_codes = SNOMED_TARGET_DISEASE_CODES.fetch(type)
diff --git a/app/models/school_move.rb b/app/models/school_move.rb
index f6782d44d1..d280dd221e 100644
--- a/app/models/school_move.rb
+++ b/app/models/school_move.rb
@@ -17,17 +17,13 @@
# index_school_moves_on_patient_id (patient_id) UNIQUE
# index_school_moves_on_patient_id_and_school_id (patient_id,school_id)
# index_school_moves_on_school_id (school_id)
-# index_school_moves_on_team_id (team_id)
#
# Foreign Keys
#
# fk_rails_... (patient_id => patients.id)
# fk_rails_... (school_id => locations.id)
-# fk_rails_... (team_id => teams.id)
#
class SchoolMove < ApplicationRecord
- self.ignored_columns = %w[home_educated team_id]
-
audited associated_with: :patient
belongs_to :patient
diff --git a/app/models/team.rb b/app/models/team.rb
index f9c3bd6af2..40ba86e39e 100644
--- a/app/models/team.rb
+++ b/app/models/team.rb
@@ -44,6 +44,9 @@ class Team < ApplicationRecord
NATIONAL_REPORTING_YEAR_GROUPS = (-2..13).to_a.freeze
+ # The number of days after the national reporting cut-off date when users will lose access to NIVS
+ NIVS_SWITCH_OFF_DELAY_DAYS = 2
+
audited associated_with: :organisation
has_associated_audits
diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb
index f1aa5f55ff..ca0f90f1ea 100644
--- a/app/models/vaccination_record.rb
+++ b/app/models/vaccination_record.rb
@@ -4,50 +4,57 @@
#
# Table name: vaccination_records
#
-# id :bigint not null, primary key
-# batch_expiry :date
-# batch_number :string
-# confirmation_sent_at :datetime
-# delivery_method :integer
-# delivery_site :integer
-# discarded_at :datetime
-# disease_types :enum not null, is an Array
-# dose_sequence :integer
-# full_dose :boolean
-# local_patient_id_uri :string
-# location_name :string
-# nhs_immunisations_api_etag :string
-# nhs_immunisations_api_identifier_system :string
-# nhs_immunisations_api_identifier_value :string
-# nhs_immunisations_api_primary_source :boolean
-# nhs_immunisations_api_sync_pending_at :datetime
-# nhs_immunisations_api_synced_at :datetime
-# notes :text
-# notify_parents :boolean
-# outcome :integer not null
-# pending_changes :jsonb not null
-# performed_at_date :date not null
-# performed_at_time :time
-# performed_by_family_name :string
-# performed_by_given_name :string
-# performed_ods_code :string
-# programme_type :enum not null
-# protocol :integer
-# source :integer not null
-# uuid :uuid not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# duplicate_of_vaccination_record_id :bigint
-# local_patient_id :string
-# location_id :bigint
-# next_dose_delay_triage_id :bigint
-# nhs_immunisations_api_id :string
-# patient_id :bigint not null
-# performed_by_user_id :bigint
-# reported_by_id :bigint
-# session_id :bigint
-# supplied_by_user_id :bigint
-# vaccine_id :bigint
+# id :bigint not null, primary key
+# batch_expiry :date
+# batch_number :string
+# confirmation_sent_at :datetime
+# delivery_method :integer
+# delivery_site :integer
+# discarded_at :datetime
+# disease_types :enum not null, is an Array
+# dose_sequence :integer
+# full_dose :boolean
+# local_patient_id_uri :string
+# location_name :string
+# nhs_immunisations_api_etag :string
+# nhs_immunisations_api_identifier_system :string
+# nhs_immunisations_api_identifier_value :string
+# nhs_immunisations_api_primary_source :boolean
+# nhs_immunisations_api_recorded_at :datetime
+# nhs_immunisations_api_snomed_procedure_code :string
+# nhs_immunisations_api_snomed_procedure_term :string
+# nhs_immunisations_api_snomed_product_code :string
+# nhs_immunisations_api_snomed_product_term :string
+# nhs_immunisations_api_snomed_reason_code :string
+# nhs_immunisations_api_snomed_reason_term :string
+# nhs_immunisations_api_sync_pending_at :datetime
+# nhs_immunisations_api_synced_at :datetime
+# notes :text
+# notify_parents :boolean
+# outcome :integer not null
+# pending_changes :jsonb not null
+# performed_at_date :date not null
+# performed_at_time :time
+# performed_by_family_name :string
+# performed_by_given_name :string
+# performed_ods_code :string
+# programme_type :enum not null
+# protocol :integer
+# source :integer not null
+# uuid :uuid not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# duplicate_of_vaccination_record_id :bigint
+# local_patient_id :string
+# location_id :bigint
+# next_dose_delay_triage_id :bigint
+# nhs_immunisations_api_id :string
+# patient_id :bigint not null
+# performed_by_user_id :bigint
+# reported_by_id :bigint
+# session_id :bigint
+# supplied_by_user_id :bigint
+# vaccine_id :bigint
#
# Indexes
#
@@ -159,6 +166,36 @@ class VaccinationRecord < ApplicationRecord
order("performed_at_date DESC, performed_at_time DESC NULLS LAST")
end
+ scope :created_or_updated_between,
+ ->(start_date, end_date) do
+ scope = all
+ if start_date.present?
+ scope =
+ scope.where(
+ "vaccination_records.created_at >= ?",
+ start_date.beginning_of_day
+ ).or(
+ scope.where(
+ "vaccination_records.updated_at >= ?",
+ start_date.beginning_of_day
+ )
+ )
+ end
+ if end_date.present?
+ scope =
+ scope.where(
+ "vaccination_records.created_at <= ?",
+ end_date.end_of_day
+ ).or(
+ scope.where(
+ "vaccination_records.updated_at <= ?",
+ end_date.end_of_day
+ )
+ )
+ end
+ scope
+ end
+
enum :protocol, { pgd: 0, psd: 1, national: 2 }, validate: { allow_nil: true }
enum :delivery_method,
diff --git a/app/policies/school_move_policy.rb b/app/policies/school_move_policy.rb
index 6b964a4774..fdd30cfc80 100644
--- a/app/policies/school_move_policy.rb
+++ b/app/policies/school_move_policy.rb
@@ -24,7 +24,7 @@ def resolve
scope
.where(patient_in_team)
.where(school: Location.generic_school)
- .or(scope.where(school: team.schools))
+ .or(scope.where(school: team.gias_schools))
.or(scope.where(school: team.generic_schools))
end
end
diff --git a/app/views/draft_consents/reason_for_refusal.html.erb b/app/views/draft_consents/reason_for_refusal.html.erb
index f37a1cfc25..8b9fc9c9b2 100644
--- a/app/views/draft_consents/reason_for_refusal.html.erb
+++ b/app/views/draft_consents/reason_for_refusal.html.erb
@@ -17,6 +17,7 @@
<%= f.govuk_radio_button :reason_for_refusal, refusal_reason.value,
label: { text: refusal_reason.label },
+ hint: { text: refusal_reason.hint },
link_errors: index.zero? %>
<% end %>
<% end %>
diff --git a/app/views/immunisation_imports/new.html.erb b/app/views/immunisation_imports/new.html.erb
index 3ad17b8797..d665128338 100644
--- a/app/views/immunisation_imports/new.html.erb
+++ b/app/views/immunisation_imports/new.html.erb
@@ -6,13 +6,21 @@
<% content_for :page_title, title %>
-<% if (cutoff_date = current_team.national_reporting_cut_off_date) && cutoff_date.today? %>
- <%= govuk_notification_banner(title_text: "#{cutoff_date.to_fs(:long)} - Check where to upload records") do |notification_banner| %>
- <% notification_banner.with_heading(text: "Vaccinations given:") %>
-
-
before <%= cutoff_date.to_fs(:long) %> must be uploaded to NIVS by the end of today
-
on or after <%= cutoff_date.to_fs(:long) %> must be uploaded to Mavis national reporting only
-
+<% if (cutoff_date = current_team.national_reporting_cut_off_date) &&
+ Date.today <= cutoff_date + Team::NIVS_SWITCH_OFF_DELAY_DAYS.days %>
+ <%= govuk_notification_banner(title_text: "Important") do %>
+
+ Mavis national reporting replaces NIVS on <%= cutoff_date.to_fs(:long) %>
+
+
+
Vaccinations given before <%= cutoff_date.to_fs(:long) %>
+
+ Upload to NIVS by <%= (cutoff_date + Team::NIVS_SWITCH_OFF_DELAY_DAYS.days).to_fs(:long) %>.
+ After this, NIVS will no longer be available.
+
+
+
Vaccinations given on or after <%= cutoff_date.to_fs(:long) %>
+
Upload to Mavis national reporting only.
<% end %>
<% end %>
diff --git a/app/views/imports/show.html.erb b/app/views/imports/show.html.erb
index 82bff9e809..b1a52a34c5 100644
--- a/app/views/imports/show.html.erb
+++ b/app/views/imports/show.html.erb
@@ -25,7 +25,7 @@
<% end %>
- Correct the issues listed, save the changes and upload the file again.
+ Refer to What your CSV file must include to correct the issues listed below, save the changes and upload the file again.
<% if import.changesets_are_invalid? %>
This may include errors identified after Mavis added a missing NHS number or replaced an incorrect NHS number.
diff --git a/app/views/inspect/graphs/show.html.erb b/app/views/inspect/graphs/show.html.erb
index de8c86e443..cfdf925ad2 100644
--- a/app/views/inspect/graphs/show.html.erb
+++ b/app/views/inspect/graphs/show.html.erb
@@ -10,6 +10,13 @@
<%= link_to "View timeline", inspect_timeline_patient_path(@primary_id), class: "nhsuk-button nhsuk-button--secondary" %>
<% end %>
+<% if @pii_access_requested_by_user && @sensitive_patient_in_graph %>
+ <%= render AppWarningCalloutComponent.new(
+ heading: "Patient is restricted",
+ description: "One of the patients in the graph has been marked as restricted. You cannot view PII for this graph.",
+ ) %>
+<% end %>
+
+ <%= render AppWarningCalloutComponent.new(
+ heading: "Patient is restricted",
+ description: "One of the patients accessed has been marked as restricted. You cannot view PII for this patient.",
+ ) %>
+
+ <% end %>
<% if @compare_patient_timeline || @invalid_patient_id || @no_events_compare_message %>
Patient <%= @patient.id %>
diff --git a/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb b/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb
index 41daeb60a2..679894fc13 100644
--- a/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb
+++ b/app/views/notify_templates/email/triage_vaccination_will_happen_mmr_second_dose.text.erb
@@ -1,10 +1,13 @@
---
template_id: "6fd910fd-120c-4e58-9ef3-15ffc5bd6edc"
template_name: triage_vaccination_will_happen_mmr_second_dose
-subject: "<%= short_patient_name %> needs another dose of the MMR vaccination"
+subject: "<%= patient.short_name %> needs another dose of the <%= programme_name_for_parents(mmr_programme.variant_for(patient:)) %> vaccination"
---
-We recently gave <%= short_patient_name %> their 1st dose of the MMR vaccination. To be fully vaccinated, they need 2 doses.
+<% variant = mmr_programme.variant_for(patient:) -%>
+<% previous_dose = patient_previous_dose_label(patient, mmr_programme, academic_year) -%>
+<% next_dose = patient_next_dose_label(patient, mmr_programme, academic_year) -%>
+We recently gave <%= patient.short_name %> their <%= previous_dose %> dose of the <%= programme_name_for_parents(variant) %> vaccination. To be fully vaccinated, they need <%= mmr_programme.maximum_dose_sequence %> doses.
-We’re coming to <%= location_name %> on <%= next_session_dates %> and plan to give <%= short_patient_name %> their 2nd dose then. Please let them know about this.
+We’re coming to <%= session.location.name %> on <%= session_future_dates(session) %> and plan to give <%= patient.short_name %> their <%= next_dose %> dose then. Please let them know about this.
-If <%= short_patient_name_apos %> health changes, or you arrange for them to get their 2nd dose somewhere else, contact us. You can email <%= subteam_email %> or phone <%= subteam_phone %>.
+If <%= patient_short_name_possessive(patient) %> health changes, or you arrange for them to get their <%= next_dose %> dose somewhere else, contact us. You can email <%= team_contact_email(session:) %> or phone <%= team_contact_phone(session:) %>.
diff --git a/app/views/notify_templates/email/vaccination_administered.text.erb b/app/views/notify_templates/email/vaccination_administered.text.erb
new file mode 100644
index 0000000000..c49abe9dcc
--- /dev/null
+++ b/app/views/notify_templates/email/vaccination_administered.text.erb
@@ -0,0 +1,37 @@
+---
+template_id: "bd51556d-bccc-469c-a822-0b88f26efd10"
+template_name: vaccination_administered
+subject: "Your child had their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination <%= vaccination_record_today_or_date(vaccination_record) %>"
+---
+<%= patient.full_name_with_known_as(context: :parents) %> had their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at <%= vaccination_record_location(vaccination_record) %> <%= vaccination_record_today_or_date(vaccination_record) %>.
+
+We suggest you record the following details somewhere.
+
+Vaccination: <%= programme_name_for_parents(vaccination_record.programme) %>
+Vaccine: <%= vaccination_record.vaccine.brand %>
+<% if vaccination_record.programme.has_multiple_vaccine_methods? -%>
+Method: <%= vaccine_method(vaccination_record.vaccine) %>
+<% end -%>
+Date of vaccination: <%= vaccination_record_date(vaccination_record) %>
+Batch number: <%= vaccination_record.batch_number %>
+
+## Possible side effects
+
+Your child might have some of the following side effects:
+
+<%= vaccine_side_effects_list(vaccination_record.vaccine) %>
+
+<% if vaccination_record.programme.mmr? %>These side effects are usually mild and only last 2 to 3 days. <% end -%>
+If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
+<% if vaccination_record.programme.multi_dose? && patient_needs_more_doses?(patient, vaccination_record.programme, academic_year) -%>
+
+## Your child still needs a <%= patient_next_dose_label(patient, vaccination_record.programme, academic_year) %> dose of the <%= programme_name_for_parents(vaccination_record.programme) %> vaccine
+
+To be fully protected against <%= programme_disease_names(vaccination_record.programme) %>, your child needs a <%= patient_next_dose_label(patient, vaccination_record.programme, academic_year) %> dose of the vaccine. Our team will be in touch about this soon.
+<% end -%>
+
+You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ’Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
+
+<%= team_contact_name(vaccination_record:) %>
+<%= team_contact_email(vaccination_record:) %>
+<%= team_contact_phone(vaccination_record:) %>
diff --git a/app/views/notify_templates/email/vaccination_administered_flu.text.erb b/app/views/notify_templates/email/vaccination_administered_flu.text.erb
deleted file mode 100644
index 5080b6890c..0000000000
--- a/app/views/notify_templates/email/vaccination_administered_flu.text.erb
+++ /dev/null
@@ -1,28 +0,0 @@
----
-template_id: "7238ee27-5840-40e5-b9b9-3130ba4cd4fa"
-template_name: vaccination_administered_flu
-subject: "Your child had their <%= vaccination %> <%= today_or_date_of_vaccination %>"
----
-<%= full_and_preferred_patient_name %> had their flu vaccination at <%= location_name %> <%= today_or_date_of_vaccination %>.
-
-We suggest you record the following details somewhere.
-
-Vaccination: <%= vaccination %>
-Vaccine: <%= vaccine_brand %>
-Method: <%= if vaccine_is?("injection") then "injection" elsif vaccine_is?("nasal") then "nasal spray" end %>
-Date of vaccination: <%= day_month_year_of_vaccination %>
-Batch number: <%= batch_name %>
-
-## Possible side effects
-
-Your child might have some of the following side effects:
-
-<%= vaccine_side_effects %>
-
-If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
-
-You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ‘Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
-
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
diff --git a/app/views/notify_templates/email/vaccination_administered_hpv.text.erb b/app/views/notify_templates/email/vaccination_administered_hpv.text.erb
deleted file mode 100644
index fd3edad7b0..0000000000
--- a/app/views/notify_templates/email/vaccination_administered_hpv.text.erb
+++ /dev/null
@@ -1,27 +0,0 @@
----
-template_id: "8a65d7b5-045c-4f26-8f76-6e593c14cb6d"
-template_name: vaccination_administered_hpv
-subject: "Your child had their HPV vaccination <%= today_or_date_of_vaccination %>"
----
-<%= full_and_preferred_patient_name %> had their HPV vaccination at <%= location_name %> <%= today_or_date_of_vaccination %>.
-
-We suggest you record the following details somewhere.
-
-Vaccination: HPV first dose
-Vaccine: Gardasil 9
-Date of vaccination: <%= day_month_year_of_vaccination %>
-Batch number: <%= batch_name %>
-
-## Possible side effects
-
-Your child might have some of the following side effects:
-
-<%= vaccine_side_effects %>
-
-If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
-
-You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ‘Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
-
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
diff --git a/app/views/notify_templates/email/vaccination_administered_menacwy.text.erb b/app/views/notify_templates/email/vaccination_administered_menacwy.text.erb
deleted file mode 100644
index 35d3b4ed14..0000000000
--- a/app/views/notify_templates/email/vaccination_administered_menacwy.text.erb
+++ /dev/null
@@ -1,27 +0,0 @@
----
-template_id: "38727494-9a81-42b3-9c1f-5c31e55333e7"
-template_name: vaccination_administered_menacwy
-subject: "Your child had their MenACWY vaccination <%= today_or_date_of_vaccination %>"
----
-<%= full_and_preferred_patient_name %> had their MenACWY vaccination at <%= location_name %> <%= today_or_date_of_vaccination %>.
-
-We suggest you record the following details somewhere.
-
-Vaccination: MenACWY
-Vaccine: MenQuadfi
-Date of vaccination: <%= day_month_year_of_vaccination %>
-Batch number: <%= batch_name %>
-
-## Possible side effects
-
-Your child might have some of the following side effects:
-
-<%= vaccine_side_effects %>
-
-If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
-
-You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ‘Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
-
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
diff --git a/app/views/notify_templates/email/vaccination_administered_mmr.text.erb b/app/views/notify_templates/email/vaccination_administered_mmr.text.erb
deleted file mode 100644
index aa0bcafaf7..0000000000
--- a/app/views/notify_templates/email/vaccination_administered_mmr.text.erb
+++ /dev/null
@@ -1,29 +0,0 @@
----
-template_id: "0b1095db-fb38-4105-9f01-a364fa8bbb1c"
-template_name: vaccination_administered_mmr
-subject: "Your child had their <%= vaccination %> <%= today_or_date_of_vaccination %>"
----
-<%= full_and_preferred_patient_name %> had their <%= vaccination %> at <%= location_name %> <%= today_or_date_of_vaccination %>.
-
-We suggest you make a note of the following details somewhere.
-
-Vaccination: <%= vaccination %>
-Vaccine: <%= vaccine_brand %>
-Date of vaccination: <%= day_month_year_of_vaccination %>
-Batch number: <%= batch_name %>
-
-## Possible side effects
-
-Your child might have some of the following side effects:
-
-<%= vaccine_side_effects %>
-
-These side effects are usually mild and only last 2 to 3 days. If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
-
-<%= mmr_second_dose_message %>
-
-You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ‘Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
-
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
diff --git a/app/views/notify_templates/email/vaccination_administered_td_ipv.text.erb b/app/views/notify_templates/email/vaccination_administered_td_ipv.text.erb
deleted file mode 100644
index b49f1a2f2d..0000000000
--- a/app/views/notify_templates/email/vaccination_administered_td_ipv.text.erb
+++ /dev/null
@@ -1,27 +0,0 @@
----
-template_id: "3abe7ca8-a889-484b-ab9f-07523302eb6a"
-template_name: vaccination_administered_td_ipv
-subject: "Your child had their Td/IPV (3-in-1 teenage booster) vaccination <%= today_or_date_of_vaccination %>"
----
-<%= full_and_preferred_patient_name %> had their Td/IPV (3-in-1 teenage booster) vaccination at <%= location_name %> <%= today_or_date_of_vaccination %>.
-
-We suggest you record the following details somewhere.
-
-Vaccination: Td/IPV
-Vaccine: Revaxis
-Date of vaccination: <%= day_month_year_of_vaccination %>
-Batch number: <%= batch_name %>
-
-##Possible side effects
-
-Your child might have some of the following side effects:
-
-<%= vaccine_side_effects %>
-
-If you’re concerned about your child’s reaction to the vaccine, contact your GP in the usual way.
-
-You can [give feedback](https://feedback.digital.nhs.uk/jfe/form/SV_3fICo6frMvUZX1k) about the ‘Give or refuse consent’ service by completing our short survey. Your feedback will help us improve the service.
-
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
diff --git a/app/views/notify_templates/email/vaccination_already_had.text.erb b/app/views/notify_templates/email/vaccination_already_had.text.erb
index d0c75d96ec..c42c5b541f 100644
--- a/app/views/notify_templates/email/vaccination_already_had.text.erb
+++ b/app/views/notify_templates/email/vaccination_already_had.text.erb
@@ -1,13 +1,13 @@
---
template_id: "e37fe0a2-7584-4c25-983a-8f5a11c818a1"
template_name: vaccination_already_had
-subject: "Cancelled <%= vaccination %> appointment for <%= short_patient_name %>"
+subject: "Cancelled <%= programme_name_for_parents(vaccination_record.programme) %> vaccination appointment for <%= short_patient_name %>"
---
-We’re contacting you because our records show that <%= short_patient_name %> was vaccinated at another location <%= today_or_date_of_vaccination %> so we’re cancelling their vaccination appointment at school.
+We’re contacting you because our records show that <%= short_patient_name %> was vaccinated at another location <%= vaccination_record_today_or_date(vaccination_record) %> so we’re cancelling their vaccination appointment at school.
-If you think this is wrong, please tell us immediately on <%= subteam_phone %> or <%= subteam_email %>.
+If you think this is wrong, please tell us immediately on <%= team_contact_phone(vaccination_record:) %> or <%= team_contact_email(vaccination_record:) %>.
-If <%= short_patient_name %> was vaccinated <%= today_or_date_of_vaccination %>, you do not need
+If <%= short_patient_name %> was vaccinated <%= vaccination_record_today_or_date(vaccination_record) %>, you do not need
to do anything.
-<%= subteam_name %>
+<%= team_contact_name(vaccination_record:) %>
diff --git a/app/views/notify_templates/email/vaccination_not_administered.text.erb b/app/views/notify_templates/email/vaccination_not_administered.text.erb
index 6065c1e27b..f08c934157 100644
--- a/app/views/notify_templates/email/vaccination_not_administered.text.erb
+++ b/app/views/notify_templates/email/vaccination_not_administered.text.erb
@@ -1,12 +1,12 @@
---
template_id: "130fe52a-014a-45dd-9f53-8e65c1b8bb79"
template_name: vaccination_not_administered
-subject: "Your child did not have their <%= vaccination %> today"
+subject: "Your child did not have their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination today"
---
-<%= full_and_preferred_patient_name %> did not have their <%= vaccination %> at school today. This was because <%= reason_did_not_vaccinate %>.
+<%= full_and_preferred_patient_name %> did not have their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at school today. This was because <%= reason_did_not_vaccinate %>.
<% if show_additional_instructions? %>If you’d still like your child to be vaccinated, you can book an appointment at a catch-up clinic by contacting our team.<% end %>
-<%= subteam_name %>
-<%= subteam_email %>
-<%= subteam_phone %>
+<%= team_contact_name(vaccination_record:) %>
+<%= team_contact_email(vaccination_record:) %>
+<%= team_contact_phone(vaccination_record:) %>
diff --git a/app/views/notify_templates/sms/vaccination_administered.text.erb b/app/views/notify_templates/sms/vaccination_administered.text.erb
index 6839aac2b4..467e8e913c 100644
--- a/app/views/notify_templates/sms/vaccination_administered.text.erb
+++ b/app/views/notify_templates/sms/vaccination_administered.text.erb
@@ -4,7 +4,7 @@ template_name: vaccination_administered
---
<%= short_patient_name %> had their <%= vaccination_and_method %> today. They might have some of the following side effects:
-<%= vaccine_side_effects %>
+<%= vaccine_side_effects_list(vaccination_record.vaccine) %>
If you're concerned, contact your GP in the usual way.
diff --git a/app/views/notify_templates/sms/vaccination_already_had.text.erb b/app/views/notify_templates/sms/vaccination_already_had.text.erb
index 960af3f334..1d9659d476 100644
--- a/app/views/notify_templates/sms/vaccination_already_had.text.erb
+++ b/app/views/notify_templates/sms/vaccination_already_had.text.erb
@@ -2,5 +2,5 @@
template_id: "fab1e355-bde1-47d5-835c-103bfd232b93"
template_name: vaccination_already_had
---
-We are cancelling <%= short_patient_name %>'s <%= vaccination %> at school
-as our records show <%= short_patient_name %> was vaccinated at another location <%= today_or_date_of_vaccination %>. If this is wrong, contact us. <%= subteam_phone %>
+We are cancelling <%= short_patient_name %>'s <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at school
+as our records show <%= short_patient_name %> was vaccinated at another location <%= vaccination_record_today_or_date(vaccination_record) %>. If this is wrong, contact us. <%= team_contact_phone(vaccination_record:) %>
diff --git a/app/views/notify_templates/sms/vaccination_not_administered.text.erb b/app/views/notify_templates/sms/vaccination_not_administered.text.erb
index b6468262bd..9f5b86c0fd 100644
--- a/app/views/notify_templates/sms/vaccination_not_administered.text.erb
+++ b/app/views/notify_templates/sms/vaccination_not_administered.text.erb
@@ -2,6 +2,6 @@
template_id: "aae061e0-b847-4d4c-a87a-12508f95a302"
template_name: vaccination_not_administered
---
-<%= short_patient_name %> did not have their <%= vaccination %> at school today. This was because <%= reason_did_not_vaccinate %>.
+<%= short_patient_name %> did not have their <%= programme_name_for_parents(vaccination_record.programme) %> vaccination at school today. This was because <%= reason_did_not_vaccinate %>.
-If you'd still like them to be vaccinated on a different date, contact the local health team by calling <%= subteam_phone %>, or email <%= subteam_email %>.
+If you'd still like them to be vaccinated on a different date, contact the local health team by calling <%= team_contact_phone(vaccination_record:) %>, or email <%= team_contact_email(vaccination_record:) %>.
diff --git a/app/views/parent_interface/consent_forms/edit/follow_up_discussion.html.erb b/app/views/parent_interface/consent_forms/edit/follow_up_discussion.html.erb
index 0bf1e41eeb..aa27533073 100644
--- a/app/views/parent_interface/consent_forms/edit/follow_up_discussion.html.erb
+++ b/app/views/parent_interface/consent_forms/edit/follow_up_discussion.html.erb
@@ -8,6 +8,8 @@
<% hint = case @consent_form.reason_for_refusal
when "contains_gelatine"
"For example, it may be possible to use a vaccine that does not contain gelatine."
+ when "do_not_want_vaccination_at_school"
+ "For example, it may be possible to vaccinate your child in a community clinic."
when "medical_reasons"
"We understand alternatives might not be suitable in some cases."
end %>
diff --git a/app/views/parent_interface/consent_forms/edit/reason_for_refusal.html.erb b/app/views/parent_interface/consent_forms/edit/reason_for_refusal.html.erb
index 27e4332452..8e90aa8ff1 100644
--- a/app/views/parent_interface/consent_forms/edit/reason_for_refusal.html.erb
+++ b/app/views/parent_interface/consent_forms/edit/reason_for_refusal.html.erb
@@ -18,6 +18,7 @@
<%= f.govuk_radio_button :reason_for_refusal,
refusal_reason.value,
label: { text: refusal_reason.label },
+ hint: { text: refusal_reason.hint },
link_errors: index.zero? %>
<% end %>
<% end %>
diff --git a/app/views/parent_interface/consent_forms/edit/reason_for_refusal_notes.html.erb b/app/views/parent_interface/consent_forms/edit/reason_for_refusal_notes.html.erb
index 4b0e27860d..18905aae93 100644
--- a/app/views/parent_interface/consent_forms/edit/reason_for_refusal_notes.html.erb
+++ b/app/views/parent_interface/consent_forms/edit/reason_for_refusal_notes.html.erb
@@ -9,7 +9,7 @@
<% programme = @consent_form.refused_consent_form_programmes.find { it.programme.mmr? }&.programme %>
<% if programme && @consent_form.reason_for_refusal == "already_vaccinated" %>
-
Children need 2 doses of the #{programme.name} vaccine to be fully protected. If your child has had more than 1 dose, give details about both doses.
+
Children need 2 doses of the <%= programme.name %> vaccine to be fully protected. If your child has had more than 1 dose, give details about both doses.