class Patient
Schema Information
Table name: patients
id :bigint not null, primary key
address_line_1 :string
address_line_2 :string
address_postcode :string
address_town :string
birth_academic_year :integer not null
date_of_birth :date not null
date_of_death :date
date_of_death_recorded_at :datetime
ethnic_background :integer
ethnic_background_other :string
ethnic_group :integer
family_name :string not null
gender_code :integer default("not_known"), not null
given_name :string not null
invalidated_at :datetime
local_authority_mhclg_code :string
nhs_number :string
pending_changes :jsonb not null
preferred_family_name :string
preferred_given_name :string
registration :string
registration_academic_year :integer
restricted_at :datetime
updated_from_pds_at :datetime
created_at :datetime not null
updated_at :datetime not null
gp_practice_id :bigint
school_id :bigint
Indexes
index_patients_on_family_name_trigram (family_name) USING gin
index_patients_on_given_name_trigram (given_name) USING gin
index_patients_on_gp_practice_id (gp_practice_id)
index_patients_on_local_authority_mhclg_code (local_authority_mhclg_code)
index_patients_on_lower_family_name_dob_address (date_of_birth, address_postcode, lower((family_name)::text))
index_patients_on_lower_given_name_dob_address (address_postcode, date_of_birth, lower((given_name)::text))
index_patients_on_lower_names_family_first_address (lower((family_name)::text), lower((given_name)::text), address_postcode)
index_patients_on_lower_names_given_first_dob_address (lower((given_name)::text), lower((family_name)::text), date_of_birth, address_postcode)
index_patients_on_names_family_first (family_name,given_name)
index_patients_on_names_given_first (given_name,family_name)
index_patients_on_nhs_number (nhs_number) UNIQUE
index_patients_on_pending_changes_not_empty (id) WHERE (pending_changes <> '{}'::jsonb)
index_patients_on_school_id (school_id)
Foreign Keys
fk_rails_... (gp_practice_id => locations.id) fk_rails_... (school_id => locations.id)
Constants
- CONSENT_GIVEN_AND_SAFE_TO_VACCINATE_STATUSES
-
These are the statuses that a patient has to be in to be considered ready
to vaccinate. The "cannot vaccinate" statuses occur if a patient is attempted to be vaccinated and then cannot be on the day for whatever reason, but we should be trying again to vaccinate these children.
Public Class Methods
Source
# File app/models/patient.rb, line 778 def self.from_consent_form(consent_form) new( address_line_1: consent_form.address_line_1, address_line_2: consent_form.address_line_2, address_postcode: consent_form.address_postcode, address_town: consent_form.address_town, birth_academic_year: consent_form.date_of_birth.academic_year, date_of_birth: consent_form.date_of_birth, family_name: consent_form.family_name, given_name: consent_form.given_name, nhs_number: consent_form.nhs_number, preferred_family_name: consent_form.preferred_family_name, preferred_given_name: consent_form.preferred_given_name, school: consent_form.school_for_school_move ) end
Public Instance Methods
Source
# File app/models/patient.rb, line 739 def apply_pending_changes_to_new_record!(changeset:) new_record = nil ActiveRecord::Base.transaction do new_record = super if changeset && new_record import = changeset.import # Parent relationships that were imported (and attached to the original # patient during matching) must move across to the duplicated patient. import .parent_relationships .where(patient_id: id) .update_all(patient_id: new_record.id) # Ensure the import itself is linked to the new patient record, not # the original one, so the import summary reflects what was created. import.patients.delete(self) import.patients << new_record end end new_record end
This method overrides the implementation in PendingChangesConcern
Calls superclass method
PendingChangesConcern#apply_pending_changes_to_new_record!
Source
# File app/models/patient.rb, line 825 def archive_due_to_deceased! archive_reasons = teams.map do |team| ArchiveReason.new(team:, patient: self, type: :deceased) end ArchiveReason.import!( archive_reasons, on_duplicate_key_update: { conflict_target: %i[team_id patient_id], columns: %i[type] } ) PatientTeamUpdater.call( patient_scope: Patient.where(id:), team_scope: Team.where(id: team_ids) ) end
Source
# File app/models/patient.rb, line 512 def archived?(team_id:) if archive_reasons.loaded? archive_reasons.any? { it.team_id == team_id } else archive_reasons.exists?(team_id:) end end
Source
# File app/models/patient.rb, line 548 def can_self_consent_after_gillick_assessment?(location:, programme_type:) gillick_assessments .where(location:, programme_type:, date: Date.current) .order(created_at: :desc) &.first &.gillick_competent? || false end
Source
# File app/models/patient.rb, line 765 def clear_pending_sessions!(team: nil) scope = patient_locations.pending unless team.nil? scope = scope.joins_sessions.where(team_locations: { team_id: team.id }) end scope.find_each do |patient_location| patient_location.end_date = Date.current patient_location.save! end end
Source
# File app/models/patient.rb, line 576 def consent_given_and_safe_to_vaccinate?(programme:, academic_year:) CONSENT_GIVEN_AND_SAFE_TO_VACCINATE_STATUSES.include?( programme_status(programme, academic_year:).status ) end
Source
# File app/models/patient.rb, line 599 def deceased? = date_of_death != nil def restricted? = restricted_at != nil def send_notifications?(team:, send_to_archived: false) !deceased? && !restricted? && !invalidated? && (send_to_archived || not_archived?(team:)) end ## # Check if a patient has been send a clinic invitation for a particular team # and academic year. # # This depends on the `clinic_notifications` association having already been # loaded. # # If multiple programmes are passed in, this method will only return `true` # if the patient has been invited for all the programmes. def invited_to_clinic?(programmes, team:, academic_year:) check_programme_types = programmes.map(&:type).uniq invited_programme_types = clinic_notifications .select { it.team_id == team.id && it.academic_year == academic_year } .flat_map(&:programme_types) .sort .uniq (check_programme_types - invited_programme_types).empty? end def update_from_pds!(pds_patient) if nhs_number.nil? || nhs_number != pds_patient.nhs_number raise NHSNumberMismatch end ActiveRecord::Base.transaction do self.date_of_death = pds_patient.date_of_death if date_of_death_changed? if date_of_death.present? archive_due_to_deceased! clear_pending_sessions! end self.date_of_death_recorded_at = Time.current end # If we've got a response from PDS we know the patient is valid, # otherwise PDS will return a 404 status. self.invalidated_at = nil if invalidated? if pds_patient.restricted self.restricted_at = Time.current unless restricted? else self.restricted_at = nil end if (ods_code = pds_patient.gp_ods_code).present? if (gp_practice = Location.gp_practice.find_by(ods_code:)) self.gp_practice = gp_practice elsif Settings.pds.raise_unknown_gp_practice Sentry.capture_exception(UnknownGPPractice.new(ods_code)) end else self.gp_practice = nil end self.updated_from_pds_at = Time.current save! end end def invalidate! return if invalidated? update!(invalidated_at: Time.current) end def not_in_team?(team:, academic_year:) if patient_locations.loaded? patient_locations.none? do |patloc| patloc.academic_year == academic_year && patloc.location.team_locations.any? do |loc| loc.academic_year == academic_year && loc.team_id == team.id end end else patient_locations .where(academic_year:) .joins(location: :team_locations) .where(team_locations: { academic_year:, team: }) .empty? end end def teams_via_patient_locations Team .joins(team_locations: { location: :patient_locations }) .merge( PatientLocation.active.where( patient_id: id, academic_year: AcademicYear.current ) ) .distinct end def dup_for_pending_changes dup.tap do |new_patient| new_patient.nhs_number = nil patient_locations.pending.find_each do |patient_location| new_patient.patient_locations.build( academic_year: patient_location.academic_year, location_id: patient_location.location_id ) end school_moves.each do |school_move| new_patient.school_moves.build( academic_year: school_move.academic_year, school_id: school_move.school_id, source: school_move.source ) end patient_teams.find_each do |patient_team| # Patients that have been duplicated from another won't have any # vaccination records or imports, therefore we need to filter the # sources. sources = patient_team.sources & %w[patient_location school_move_school] new_patient.patient_teams.build(team_id: patient_team.team_id, sources:) end end end # This method overrides the implementation in `PendingChangesConcern` def apply_pending_changes_to_new_record!(changeset:) new_record = nil ActiveRecord::Base.transaction do new_record = super if changeset && new_record import = changeset.import # Parent relationships that were imported (and attached to the original # patient during matching) must move across to the duplicated patient. import .parent_relationships .where(patient_id: id) .update_all(patient_id: new_record.id) # Ensure the import itself is linked to the new patient record, not # the original one, so the import summary reflects what was created. import.patients.delete(self) import.patients << new_record end end new_record end def clear_pending_sessions!(team: nil) scope = patient_locations.pending unless team.nil? scope = scope.joins_sessions.where(team_locations: { team_id: team.id }) end scope.find_each do |patient_location| patient_location.end_date = Date.current patient_location.save! end end def self.from_consent_form(consent_form) new( address_line_1: consent_form.address_line_1, address_line_2: consent_form.address_line_2, address_postcode: consent_form.address_postcode, address_town: consent_form.address_town, birth_academic_year: consent_form.date_of_birth.academic_year, date_of_birth: consent_form.date_of_birth, family_name: consent_form.family_name, given_name: consent_form.given_name, nhs_number: consent_form.nhs_number, preferred_family_name: consent_form.preferred_family_name, preferred_given_name: consent_form.preferred_given_name, school: consent_form.school_for_school_move ) end class NHSNumberMismatch < StandardError end class UnknownGPPractice < StandardError end def latest_pds_search_result nhs_numbers = pds_search_results.latest_set&.pluck(:nhs_number)&.compact&.uniq nhs_numbers&.one? ? nhs_numbers.first : nil end def pds_lookup_match? nhs_number.present? && nhs_number == latest_pds_search_result end def notifier = Notifier::Patient.new(self) private def locations_are_correct_type if gp_practice && !gp_practice.gp_practice? errors.add(:gp_practice, "must be a GP practice location type") end if school && !school.school? errors.add(:school, "must be a school location type") end end def archive_due_to_deceased! archive_reasons = teams.map do |team| ArchiveReason.new(team:, patient: self, type: :deceased) end ArchiveReason.import!( archive_reasons, on_duplicate_key_update: { conflict_target: %i[team_id patient_id], columns: %i[type] } ) PatientTeamUpdater.call( patient_scope: Patient.where(id:), team_scope: Team.where(id: team_ids) ) end def fhir_mapper = @fhir_mapper ||= FHIRMapper::Patient.new(self) def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id])
Source
# File app/models/patient.rb, line 708 def dup_for_pending_changes dup.tap do |new_patient| new_patient.nhs_number = nil patient_locations.pending.find_each do |patient_location| new_patient.patient_locations.build( academic_year: patient_location.academic_year, location_id: patient_location.location_id ) end school_moves.each do |school_move| new_patient.school_moves.build( academic_year: school_move.academic_year, school_id: school_move.school_id, source: school_move.source ) end patient_teams.find_each do |patient_team| # Patients that have been duplicated from another won't have any # vaccination records or imports, therefore we need to filter the # sources. sources = patient_team.sources & %w[patient_location school_move_school] new_patient.patient_teams.build(team_id: patient_team.team_id, sources:) end end end
Source
# File app/models/patient.rb, line 595 def eligible_for_mmrv? date_of_birth >= Programme::MIN_MMRV_ELIGIBILITY_DATE end
Source
# File app/models/patient.rb, line 845 def fhir_mapper = @fhir_mapper ||= FHIRMapper::Patient.new(self) def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id]) end end end
Source
# File app/models/patient.rb, line 873 def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id]) end end
Source
# File app/models/patient.rb, line 572 def has_patient_specific_direction?(team:, **kwargs) patient_specific_directions.not_invalidated.where(team:, **kwargs).exists? end
Source
# File app/models/patient.rb, line 673 def invalidate! return if invalidated? update!(invalidated_at: Time.current) end
Source
# File app/models/patient.rb, line 617 def invited_to_clinic?(programmes, team:, academic_year:) check_programme_types = programmes.map(&:type).uniq invited_programme_types = clinic_notifications .select { it.team_id == team.id && it.academic_year == academic_year } .flat_map(&:programme_types) .sort .uniq (check_programme_types - invited_programme_types).empty? end
Check if a patient has been send a clinic invitation for a particular team and academic year.
This depends on the clinic_notifications association having already been loaded.
If multiple programmes are passed in, this method will only return true if the patient has been invited for all the programmes.
Source
# File app/models/patient.rb, line 586 def latest_delayed_triage(programme_types:) triages .not_invalidated .where(programme_type: programme_types) .delay_vaccination .order(created_at: :desc) .first end
Source
# File app/models/patient.rb, line 801 def latest_pds_search_result nhs_numbers = pds_search_results.latest_set&.pluck(:nhs_number)&.compact&.uniq nhs_numbers&.one? ? nhs_numbers.first : nil end
Source
# File app/models/patient.rb, line 815 def locations_are_correct_type if gp_practice && !gp_practice.gp_practice? errors.add(:gp_practice, "must be a GP practice location type") end if school && !school.school? errors.add(:school, "must be a school location type") end end
Source
# File app/models/patient.rb, line 520 def not_archived?(team:) !archive_reasons.exists?(team:) end
Source
# File app/models/patient.rb, line 679 def not_in_team?(team:, academic_year:) if patient_locations.loaded? patient_locations.none? do |patloc| patloc.academic_year == academic_year && patloc.location.team_locations.any? do |loc| loc.academic_year == academic_year && loc.team_id == team.id end end else patient_locations .where(academic_year:) .joins(location: :team_locations) .where(team_locations: { academic_year:, team: }) .empty? end end
Source
# File app/models/patient.rb, line 811 def notifier = Notifier::Patient.new(self) private def locations_are_correct_type if gp_practice && !gp_practice.gp_practice? errors.add(:gp_practice, "must be a GP practice location type") end if school && !school.school? errors.add(:school, "must be a school location type") end end def archive_due_to_deceased! archive_reasons = teams.map do |team| ArchiveReason.new(team:, patient: self, type: :deceased) end ArchiveReason.import!( archive_reasons, on_duplicate_key_update: { conflict_target: %i[team_id patient_id], columns: %i[type] } ) PatientTeamUpdater.call( patient_scope: Patient.where(id:), team_scope: Team.where(id: team_ids) ) end def fhir_mapper = @fhir_mapper ||= FHIRMapper::Patient.new(self) def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id]) end end
Source
# File app/models/patient.rb, line 807 def pds_lookup_match? nhs_number.present? && nhs_number == latest_pds_search_result end
Source
# File app/models/patient.rb, line 556 def programme_status(programme, academic_year:) # TODO: Update this method to accept the `programme_type` so that we can # then determine the right programme variant from the `disease_types` on # the `Patient::ProgrammeStatus`. programme_type = programme.type programme_statuses.find do it.programme_type == programme_type && it.academic_year == academic_year end || programme_statuses.build(programme_type:, academic_year:) end
Source
# File app/models/patient.rb, line 567 def registration_status(session:) registration_statuses.find { it.session_id == session.id } || registration_statuses.build(session:) end
Source
# File app/models/patient.rb, line 601 def restricted? = restricted_at != nil def send_notifications?(team:, send_to_archived: false) !deceased? && !restricted? && !invalidated? && (send_to_archived || not_archived?(team:)) end ## # Check if a patient has been send a clinic invitation for a particular team # and academic year. # # This depends on the `clinic_notifications` association having already been # loaded. # # If multiple programmes are passed in, this method will only return `true` # if the patient has been invited for all the programmes. def invited_to_clinic?(programmes, team:, academic_year:) check_programme_types = programmes.map(&:type).uniq invited_programme_types = clinic_notifications .select { it.team_id == team.id && it.academic_year == academic_year } .flat_map(&:programme_types) .sort .uniq (check_programme_types - invited_programme_types).empty? end def update_from_pds!(pds_patient) if nhs_number.nil? || nhs_number != pds_patient.nhs_number raise NHSNumberMismatch end ActiveRecord::Base.transaction do self.date_of_death = pds_patient.date_of_death if date_of_death_changed? if date_of_death.present? archive_due_to_deceased! clear_pending_sessions! end self.date_of_death_recorded_at = Time.current end # If we've got a response from PDS we know the patient is valid, # otherwise PDS will return a 404 status. self.invalidated_at = nil if invalidated? if pds_patient.restricted self.restricted_at = Time.current unless restricted? else self.restricted_at = nil end if (ods_code = pds_patient.gp_ods_code).present? if (gp_practice = Location.gp_practice.find_by(ods_code:)) self.gp_practice = gp_practice elsif Settings.pds.raise_unknown_gp_practice Sentry.capture_exception(UnknownGPPractice.new(ods_code)) end else self.gp_practice = nil end self.updated_from_pds_at = Time.current save! end end def invalidate! return if invalidated? update!(invalidated_at: Time.current) end def not_in_team?(team:, academic_year:) if patient_locations.loaded? patient_locations.none? do |patloc| patloc.academic_year == academic_year && patloc.location.team_locations.any? do |loc| loc.academic_year == academic_year && loc.team_id == team.id end end else patient_locations .where(academic_year:) .joins(location: :team_locations) .where(team_locations: { academic_year:, team: }) .empty? end end def teams_via_patient_locations Team .joins(team_locations: { location: :patient_locations }) .merge( PatientLocation.active.where( patient_id: id, academic_year: AcademicYear.current ) ) .distinct end def dup_for_pending_changes dup.tap do |new_patient| new_patient.nhs_number = nil patient_locations.pending.find_each do |patient_location| new_patient.patient_locations.build( academic_year: patient_location.academic_year, location_id: patient_location.location_id ) end school_moves.each do |school_move| new_patient.school_moves.build( academic_year: school_move.academic_year, school_id: school_move.school_id, source: school_move.source ) end patient_teams.find_each do |patient_team| # Patients that have been duplicated from another won't have any # vaccination records or imports, therefore we need to filter the # sources. sources = patient_team.sources & %w[patient_location school_move_school] new_patient.patient_teams.build(team_id: patient_team.team_id, sources:) end end end # This method overrides the implementation in `PendingChangesConcern` def apply_pending_changes_to_new_record!(changeset:) new_record = nil ActiveRecord::Base.transaction do new_record = super if changeset && new_record import = changeset.import # Parent relationships that were imported (and attached to the original # patient during matching) must move across to the duplicated patient. import .parent_relationships .where(patient_id: id) .update_all(patient_id: new_record.id) # Ensure the import itself is linked to the new patient record, not # the original one, so the import summary reflects what was created. import.patients.delete(self) import.patients << new_record end end new_record end def clear_pending_sessions!(team: nil) scope = patient_locations.pending unless team.nil? scope = scope.joins_sessions.where(team_locations: { team_id: team.id }) end scope.find_each do |patient_location| patient_location.end_date = Date.current patient_location.save! end end def self.from_consent_form(consent_form) new( address_line_1: consent_form.address_line_1, address_line_2: consent_form.address_line_2, address_postcode: consent_form.address_postcode, address_town: consent_form.address_town, birth_academic_year: consent_form.date_of_birth.academic_year, date_of_birth: consent_form.date_of_birth, family_name: consent_form.family_name, given_name: consent_form.given_name, nhs_number: consent_form.nhs_number, preferred_family_name: consent_form.preferred_family_name, preferred_given_name: consent_form.preferred_given_name, school: consent_form.school_for_school_move ) end class NHSNumberMismatch < StandardError end class UnknownGPPractice < StandardError end def latest_pds_search_result nhs_numbers = pds_search_results.latest_set&.pluck(:nhs_number)&.compact&.uniq nhs_numbers&.one? ? nhs_numbers.first : nil end def pds_lookup_match? nhs_number.present? && nhs_number == latest_pds_search_result end def notifier = Notifier::Patient.new(self) private def locations_are_correct_type if gp_practice && !gp_practice.gp_practice? errors.add(:gp_practice, "must be a GP practice location type") end if school && !school.school? errors.add(:school, "must be a school location type") end end def archive_due_to_deceased! archive_reasons = teams.map do |team| ArchiveReason.new(team:, patient: self, type: :deceased) end ArchiveReason.import!( archive_reasons, on_duplicate_key_update: { conflict_target: %i[team_id patient_id], columns: %i[type] } ) PatientTeamUpdater.call( patient_scope: Patient.where(id:), team_scope: Team.where(id: team_ids) ) end def fhir_mapper = @fhir_mapper ||= FHIRMapper::Patient.new(self) def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id]) end
Source
# File app/models/patient.rb, line 861 def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end
Source
# File app/models/patient.rb, line 603 def send_notifications?(team:, send_to_archived: false) !deceased? && !restricted? && !invalidated? && (send_to_archived || not_archived?(team:)) end
Source
# File app/models/patient.rb, line 503 def sessions Session .joins_patient_locations .joins_patients .joins_session_programme_year_groups .where(patients: { id: }) .distinct end
Source
# File app/models/patient.rb, line 867 def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end
Source
# File app/models/patient.rb, line 857 def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end
Source
# File app/models/patient.rb, line 847 def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end
Source
# File app/models/patient.rb, line 530 def show_year_group?(team:) return false if team.has_national_reporting_access? academic_year = AcademicYear.pending year_group = self.year_group(academic_year:) location_programme_year_groups = school&.location_programme_year_groups || team.location_programme_year_groups team.programmes.any? do |programme| location_programme_year_groups.any? do it.programme_type == programme.type && it.academic_year == academic_year && it.year_group == year_group end end end
Source
# File app/models/patient.rb, line 851 def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end
Source
# File app/models/patient.rb, line 696 def teams_via_patient_locations Team .joins(team_locations: { location: :patient_locations }) .merge( PatientLocation.active.where( patient_id: id, academic_year: AcademicYear.current ) ) .distinct end
Source
# File app/models/patient.rb, line 630 def update_from_pds!(pds_patient) if nhs_number.nil? || nhs_number != pds_patient.nhs_number raise NHSNumberMismatch end ActiveRecord::Base.transaction do self.date_of_death = pds_patient.date_of_death if date_of_death_changed? if date_of_death.present? archive_due_to_deceased! clear_pending_sessions! end self.date_of_death_recorded_at = Time.current end # If we've got a response from PDS we know the patient is valid, # otherwise PDS will return a 404 status. self.invalidated_at = nil if invalidated? if pds_patient.restricted self.restricted_at = Time.current unless restricted? else self.restricted_at = nil end if (ods_code = pds_patient.gp_ods_code).present? if (gp_practice = Location.gp_practice.find_by(ods_code:)) self.gp_practice = gp_practice elsif Settings.pds.raise_unknown_gp_practice Sentry.capture_exception(UnknownGPPractice.new(ods_code)) end else self.gp_practice = nil end self.updated_from_pds_at = Time.current save! end end
Source
# File app/models/patient.rb, line 582 def vaccine_criteria(programme:, academic_year:) programme_status(programme, academic_year:).vaccine_criteria end
Source
# File app/models/patient.rb, line 524 def year_group(academic_year:) birth_academic_year.to_year_group(academic_year:) end
Source
# File app/models/patient.rb, line 528 def year_group_changed? = birth_academic_year_changed? def show_year_group?(team:) return false if team.has_national_reporting_access? academic_year = AcademicYear.pending year_group = self.year_group(academic_year:) location_programme_year_groups = school&.location_programme_year_groups || team.location_programme_year_groups team.programmes.any? do |programme| location_programme_year_groups.any? do it.programme_type == programme.type && it.academic_year == academic_year && it.year_group == year_group end end end def can_self_consent_after_gillick_assessment?(location:, programme_type:) gillick_assessments .where(location:, programme_type:, date: Date.current) .order(created_at: :desc) &.first &.gillick_competent? || false end def programme_status(programme, academic_year:) # TODO: Update this method to accept the `programme_type` so that we can # then determine the right programme variant from the `disease_types` on # the `Patient::ProgrammeStatus`. programme_type = programme.type programme_statuses.find do it.programme_type == programme_type && it.academic_year == academic_year end || programme_statuses.build(programme_type:, academic_year:) end def registration_status(session:) registration_statuses.find { it.session_id == session.id } || registration_statuses.build(session:) end def has_patient_specific_direction?(team:, **kwargs) patient_specific_directions.not_invalidated.where(team:, **kwargs).exists? end def consent_given_and_safe_to_vaccinate?(programme:, academic_year:) CONSENT_GIVEN_AND_SAFE_TO_VACCINATE_STATUSES.include?( programme_status(programme, academic_year:).status ) end def vaccine_criteria(programme:, academic_year:) programme_status(programme, academic_year:).vaccine_criteria end def latest_delayed_triage(programme_types:) triages .not_invalidated .where(programme_type: programme_types) .delay_vaccination .order(created_at: :desc) .first end def eligible_for_mmrv? date_of_birth >= Programme::MIN_MMRV_ELIGIBILITY_DATE end def deceased? = date_of_death != nil def restricted? = restricted_at != nil def send_notifications?(team:, send_to_archived: false) !deceased? && !restricted? && !invalidated? && (send_to_archived || not_archived?(team:)) end ## # Check if a patient has been send a clinic invitation for a particular team # and academic year. # # This depends on the `clinic_notifications` association having already been # loaded. # # If multiple programmes are passed in, this method will only return `true` # if the patient has been invited for all the programmes. def invited_to_clinic?(programmes, team:, academic_year:) check_programme_types = programmes.map(&:type).uniq invited_programme_types = clinic_notifications .select { it.team_id == team.id && it.academic_year == academic_year } .flat_map(&:programme_types) .sort .uniq (check_programme_types - invited_programme_types).empty? end def update_from_pds!(pds_patient) if nhs_number.nil? || nhs_number != pds_patient.nhs_number raise NHSNumberMismatch end ActiveRecord::Base.transaction do self.date_of_death = pds_patient.date_of_death if date_of_death_changed? if date_of_death.present? archive_due_to_deceased! clear_pending_sessions! end self.date_of_death_recorded_at = Time.current end # If we've got a response from PDS we know the patient is valid, # otherwise PDS will return a 404 status. self.invalidated_at = nil if invalidated? if pds_patient.restricted self.restricted_at = Time.current unless restricted? else self.restricted_at = nil end if (ods_code = pds_patient.gp_ods_code).present? if (gp_practice = Location.gp_practice.find_by(ods_code:)) self.gp_practice = gp_practice elsif Settings.pds.raise_unknown_gp_practice Sentry.capture_exception(UnknownGPPractice.new(ods_code)) end else self.gp_practice = nil end self.updated_from_pds_at = Time.current save! end end def invalidate! return if invalidated? update!(invalidated_at: Time.current) end def not_in_team?(team:, academic_year:) if patient_locations.loaded? patient_locations.none? do |patloc| patloc.academic_year == academic_year && patloc.location.team_locations.any? do |loc| loc.academic_year == academic_year && loc.team_id == team.id end end else patient_locations .where(academic_year:) .joins(location: :team_locations) .where(team_locations: { academic_year:, team: }) .empty? end end def teams_via_patient_locations Team .joins(team_locations: { location: :patient_locations }) .merge( PatientLocation.active.where( patient_id: id, academic_year: AcademicYear.current ) ) .distinct end def dup_for_pending_changes dup.tap do |new_patient| new_patient.nhs_number = nil patient_locations.pending.find_each do |patient_location| new_patient.patient_locations.build( academic_year: patient_location.academic_year, location_id: patient_location.location_id ) end school_moves.each do |school_move| new_patient.school_moves.build( academic_year: school_move.academic_year, school_id: school_move.school_id, source: school_move.source ) end patient_teams.find_each do |patient_team| # Patients that have been duplicated from another won't have any # vaccination records or imports, therefore we need to filter the # sources. sources = patient_team.sources & %w[patient_location school_move_school] new_patient.patient_teams.build(team_id: patient_team.team_id, sources:) end end end # This method overrides the implementation in `PendingChangesConcern` def apply_pending_changes_to_new_record!(changeset:) new_record = nil ActiveRecord::Base.transaction do new_record = super if changeset && new_record import = changeset.import # Parent relationships that were imported (and attached to the original # patient during matching) must move across to the duplicated patient. import .parent_relationships .where(patient_id: id) .update_all(patient_id: new_record.id) # Ensure the import itself is linked to the new patient record, not # the original one, so the import summary reflects what was created. import.patients.delete(self) import.patients << new_record end end new_record end def clear_pending_sessions!(team: nil) scope = patient_locations.pending unless team.nil? scope = scope.joins_sessions.where(team_locations: { team_id: team.id }) end scope.find_each do |patient_location| patient_location.end_date = Date.current patient_location.save! end end def self.from_consent_form(consent_form) new( address_line_1: consent_form.address_line_1, address_line_2: consent_form.address_line_2, address_postcode: consent_form.address_postcode, address_town: consent_form.address_town, birth_academic_year: consent_form.date_of_birth.academic_year, date_of_birth: consent_form.date_of_birth, family_name: consent_form.family_name, given_name: consent_form.given_name, nhs_number: consent_form.nhs_number, preferred_family_name: consent_form.preferred_family_name, preferred_given_name: consent_form.preferred_given_name, school: consent_form.school_for_school_move ) end class NHSNumberMismatch < StandardError end class UnknownGPPractice < StandardError end def latest_pds_search_result nhs_numbers = pds_search_results.latest_set&.pluck(:nhs_number)&.compact&.uniq nhs_numbers&.one? ? nhs_numbers.first : nil end def pds_lookup_match? nhs_number.present? && nhs_number == latest_pds_search_result end def notifier = Notifier::Patient.new(self) private def locations_are_correct_type if gp_practice && !gp_practice.gp_practice? errors.add(:gp_practice, "must be a GP practice location type") end if school && !school.school? errors.add(:school, "must be a school location type") end end def archive_due_to_deceased! archive_reasons = teams.map do |team| ArchiveReason.new(team:, patient: self, type: :deceased) end ArchiveReason.import!( archive_reasons, on_duplicate_key_update: { conflict_target: %i[team_id patient_id], columns: %i[type] } ) PatientTeamUpdater.call( patient_scope: Patient.where(id:), team_scope: Team.where(id: team_ids) ) end def fhir_mapper = @fhir_mapper ||= FHIRMapper::Patient.new(self) def should_sync_vaccinations_to_nhs_immunisations_api? nhs_number_previously_changed? || invalidated_at_previously_changed? end def sync_vaccinations_to_nhs_immunisations_api if should_sync_vaccinations_to_nhs_immunisations_api? vaccination_records.sync_all_to_nhs_immunisations_api end end def should_search_vaccinations_from_nhs_immunisations_api? nhs_number_previously_changed? end def search_vaccinations_from_nhs_immunisations_api if should_search_vaccinations_from_nhs_immunisations_api? SearchVaccinationRecordsInNHSJob.perform_async(id) end end def should_generate_important_notice? nhs_number_previously_changed? || invalidated_at_previously_changed? || restricted_at_previously_changed? || date_of_death_recorded_at_previously_changed? end def generate_important_notice_if_needed if should_generate_important_notice? ImportantNoticeGeneratorJob.perform_later([id])