class PatientChangeset
Schema Information
Table name: patient_changesets
id :bigint not null, primary key
data :jsonb
import_type :string not null
matched_on_nhs_number :boolean
pds_nhs_number :string
processed_at :datetime
record_type :integer default("new_patient"), not null
row_number :integer
status :integer default("pending"), not null
uploaded_nhs_number :string
created_at :datetime not null
updated_at :datetime not null
import_id :bigint not null
patient_id :bigint
school_id :bigint
Indexes
index_patient_changesets_on_import (import_type,import_id) index_patient_changesets_on_patient_id (patient_id) index_patient_changesets_on_status (status)
Foreign Keys
fk_rails_... (patient_id => patients.id) fk_rails_... (school_id => locations.id)
Public Class Methods
Source
# File app/models/patient_changeset.rb, line 129 def self.from_import_row(row:, import:, row_number:) create!( import:, row_number:, school: row.school, uploaded_nhs_number: row.import_attributes[:nhs_number], data: { upload: { child: row.import_attributes, academic_year: row.academic_year, # TODO: This should gotten from the import, but it does not provide. # Maybe one day. school_move_source: row.school_move_source, parent_1: row.parent_1_import_attributes, parent_2: row.parent_2_import_attributes }, search_results: [], review: { patient: { }, school_move: { } } } ) end
Public Instance Methods
Source
# File app/models/patient_changeset.rb, line 176 def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0) +
Source
# File app/models/patient_changeset.rb, line 168 def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number
Source
# File app/models/patient_changeset.rb, line 249 def assign_patient_id self.patient_id = patient.id if patient.persisted? end
Source
# File app/models/patient_changeset.rb, line 353 def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end
Source
# File app/models/patient_changeset.rb, line 390 def auto_accept_registration? import_type == "ClassImport" end
Source
# File app/models/patient_changeset.rb, line 436 def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end
Source
# File app/models/patient_changeset.rb, line 178 def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0) + 2
Source
# File app/models/patient_changeset.rb, line 477 def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end
Source
# File app/models/patient_changeset.rb, line 465 def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end
Source
# File app/models/patient_changeset.rb, line 160 def child_attributes = data["upload"]["child"] def family_name = child_attributes["family_name"] def given_name = child_attributes["given_name"] def date_of_birth = child_attributes["date_of_birth"] def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end
Source
# File app/models/patient_changeset.rb, line 498 def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end
Source
# File app/models/patient_changeset.rb, line 507 def csv_row_number (row_number || 0) + 2 end
Source
# File app/models/patient_changeset.rb, line 166 def date_of_birth = child_attributes["date_of_birth"] def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number
Source
# File app/models/patient_changeset.rb, line 394 def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end
Source
# File app/models/patient_changeset.rb, line 312 def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end
Source
# File app/models/patient_changeset.rb, line 162 def family_name = child_attributes["family_name"] def given_name = child_attributes["given_name"] def date_of_birth = child_attributes["date_of_birth"] def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def
Source
# File app/models/patient_changeset.rb, line 164 def given_name = child_attributes["given_name"] def date_of_birth = child_attributes["date_of_birth"] def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number
Source
# File app/models/patient_changeset.rb, line 411 def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end
Source
# File app/models/patient_changeset.rb, line 405 def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end
Source
# File app/models/patient_changeset.rb, line 191 def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end
Source
# File app/models/patient_changeset.rb, line 182 def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0) + 2 end end
Source
# File app/models/patient_changeset.rb, line 170 def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number ||
Source
# File app/models/patient_changeset.rb, line 172 def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0
Source
# File app/models/patient_changeset.rb, line 253 def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end
Source
# File app/models/patient_changeset.rb, line 230 def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end
Source
# File app/models/patient_changeset.rb, line 209 def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end
Source
# File app/models/patient_changeset.rb, line 199 def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end
Calls superclass method
Source
# File app/models/patient_changeset.rb, line 184 def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end
Source
# File app/models/patient_changeset.rb, line 345 def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end
Source
# File app/models/patient_changeset.rb, line 195 def processed! update!(status: :processed, processed_at: Time.zone.now) end
Source
# File app/models/patient_changeset.rb, line 503 def reset_patient_id! update_column(:patient_id, nil) end
Source
# File app/models/patient_changeset.rb, line 158 def review_data = data["review"] def child_attributes = data["upload"]["child"] def family_name = child_attributes["family_name"] def given_name = child_attributes["given_name"] def date_of_birth = child_attributes["date_of_birth"] def address_postcode = child_attributes["address_postcode"] def parent_1_attributes = data["upload"]["parent_1"] def parent_2_attributes = data["upload"]["parent_2"] def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end
Source
# File app/models/patient_changeset.rb, line 268 def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end
Source
# File app/models/patient_changeset.rb, line 180 def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0) + 2 end
Source
# File app/models/patient_changeset.rb, line 298 def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end
Source
# File app/models/patient_changeset.rb, line 174 def search_results = data["search_results"] def academic_year = data["upload"]["academic_year"] def birth_academic_year = child_attributes["birth_academic_year"] def school_move_source = data["upload"]["school_move_source"] def nhs_number = child_attributes["nhs_number"] def pending_changes unless review_data["patient"] && review_data["patient"]["pending_changes"] return end review_data["patient"]["pending_changes"] || {} end def invalidate! data["upload"]["child"]["invalidated_at"] = Time.current end def processed! update!(status: :processed, processed_at: Time.zone.now) end def patient return super if patient_id.present? @patient ||= if (existing_patient = existing_patients.first) prepare_patient_changes(existing_patient) else Patient.new(child_attributes.merge("patient_locations" => [])) end end def parents @parents ||= [parent_1_attributes.presence, parent_2_attributes.presence].compact .map do |attrs| parent = Parent.match_existing( patient: existing_patients.first, email: attrs["email"], phone: attrs["phone"], full_name: attrs["full_name"] ) || Parent.new parent.email = attrs["email"] if attrs["email"] parent.full_name = attrs["full_name"] if attrs["full_name"] parent.phone = attrs["phone"] if attrs["phone"] parent.phone_receive_updates = false if parent.phone.blank? parent end end def parent_relationships relationships = [parent_1_attributes, parent_2_attributes].filter_map do |attrs| next if attrs.blank? parent_relationship_attributes( attrs["relationship"].presence || "unknown" ) end @parent_relationships ||= relationships .zip(parents) .map do |relationship, parent| ParentRelationship .find_or_initialize_by(parent:, patient:) .tap { it.assign_attributes(relationship) } end end def assign_patient_id self.patient_id = patient.id if patient.persisted? end def parent_relationship_attributes(relationship) case relationship&.downcase when nil, "unknown" { type: "unknown" } when "mother", "mum" { type: "mother" } when "father", "dad" { type: "father" } when "guardian" { type: "guardian" } else { type: "other", other_name: relationship } end end def school_move @school_move ||= begin return if patient.deceased? if import_type == "CohortImport" && school_move_to_unknown_school_from_another_team? return end if patient.new_record? || patient.school != school || patient.not_in_team?(team:, academic_year:) || patient.archived?(team_id: team.id) || patient.school_moves.any? school_move = patient.school_moves.includes(:teams).first || SchoolMove.new(patient:) school_move.assign_attributes( academic_year:, school:, source: school_move_source ) # TODO: Figure out why this is necessary. school_move.strict_loading!(false) school_move end end end def school_move_to_unknown_school_from_another_team? return false unless patient.persisted? from_known_school = patient.school && patient.school&.urn != Location::URN_UNKNOWN to_known_school = school && school&.urn != Location::URN_UNKNOWN moving_to_unknown_school = from_known_school && !to_known_school from_another_team = !patient.teams_via_patient_locations.include?(team) moving_to_unknown_school && from_another_team end def existing_patients deserialize_attribute(child_attributes, "date_of_birth", :date) deserialize_attribute(child_attributes, "invalidated_at", :datetime) if child_attributes["given_name"].blank? || child_attributes["family_name"].blank? || child_attributes["date_of_birth"]&.nil? return [] end matches = PatientMatcher.from_relation( Patient.includes(:patient_locations, school_moves: :teams), nhs_number: child_attributes["nhs_number"], given_name: child_attributes["given_name"], family_name: child_attributes["family_name"], date_of_birth: child_attributes["date_of_birth"], address_postcode: child_attributes["address_postcode"] ) existing_nhs_number = matches.first.nhs_number if matches.count == 1 upload_nhs_number = child_attributes["nhs_number"] self.matched_on_nhs_number = if existing_nhs_number && existing_nhs_number == upload_nhs_number true else false end matches end def prepare_patient_changes(existing_patient) auto_accept_child_attributes(existing_patient) handle_address_updates(existing_patient) stage_and_handle_pending_changes(existing_patient) existing_patient end def auto_accept_child_attributes(existing_patient) set_child_attribute_if_valid(:preferred_given_name, existing_patient) set_child_attribute_if_valid(:preferred_family_name, existing_patient) set_child_attribute_if_valid( :gender_code, existing_patient, %w[male female not_specified] ) set_child_attribute_if_valid(:nhs_number, existing_patient) if auto_accept_registration? set_child_attribute_if_valid(:registration, existing_patient) set_child_attribute_if_valid( :registration_academic_year, existing_patient ) end end def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end def auto_accept_registration? import_type == "ClassImport" end def deserialize_attribute(attributes, key, type) if attributes.key?(key) && attributes[key].is_a?(String) attributes[key] = case type when :date attributes[key].to_date when :datetime Time.iso8601(attributes[key]) end end end def inter_team_move? return false unless patient_id.present? && school_move.present? school_move.from_another_team? end def handle_address_updates(existing_patient) if child_attributes["address_postcode"].present? && child_attributes["address_postcode"] != existing_patient.address_postcode # If the postcode is different then we need to reset any address fields # that were nil to ensure they when we merge we don't leave any old # values. # # NOTE: This may also be achievable by removing the `&` from # `address_line_1&.to_s` in PatientImportRow#import_attributes, but # this needs to be tested. child_attributes["address_line_1"] ||= nil child_attributes["address_line_2"] ||= nil child_attributes["address_town"] ||= nil elsif auto_overwrite_address?(existing_patient) existing_patient.assign_attributes( child_attributes.slice( "address_line_1", "address_line_2", "address_town" ).compact ) end end def auto_overwrite_address?(existing_patient) existing_patient.address_postcode == child_attributes["address_postcode"] && [ child_attributes["address_line_1"], child_attributes["address_line_2"], child_attributes["address_town"] ].any?(&:present?) end def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end def changeset_type if patient.id.nil? :new_patient elsif school_move_to_unknown_school_from_another_team? :skipped_school_move elsif patient.pending_changes.any? :import_issue else :auto_match end end def calculate_review_data! clear_review_data! reset_patient_id! if patient_id.present? update_column(:record_type, changeset_type) return if new_patient? data["review"]["patient"]["id"] = patient.id if patient.pending_changes.any? data["review"]["patient"]["pending_changes"] = patient.pending_changes end if school_move.present? && !has_auto_confirmable_school_move?(school_move, import) data["review"]["school_move"]["school_id"] = school_move.school_id end save! end def clear_review_data! data["review"] = { patient: {}, school_move: {} } save! end def reset_patient_id! update_column(:patient_id, nil) end def csv_row_number (row_number || 0)
Source
# File app/models/patient_changeset.rb, line 372 def set_child_attribute_if_valid( attribute, existing_patient, valid_values = nil ) if valid_values.nil? in_pending_changes = child_attributes[attribute.to_s].present? in_existing_patient = existing_patient[attribute.to_s].present? else in_pending_changes = child_attributes[attribute.to_s].in?(valid_values) in_existing_patient = existing_patient[attribute.to_s].in?(valid_values) end if in_pending_changes && !in_existing_patient existing_patient[attribute] = child_attributes[attribute.to_s] end end
Source
# File app/models/patient_changeset.rb, line 445 def stage_and_handle_pending_changes(existing_patient) auto_accepted_changes = existing_patient.changed_attributes.keys existing_patient.stage_changes(child_attributes) # If there are pending changes that require review, we need to revert # any auto-accepted changes and move them to pending_changes instead. # This ensures all changes are reviewed together rather than having # some changes applied immediately while others await review. This # is particularly critical when handling potential duplicates like twins, # where auto-accepting some changes could lead to data from one twin being # incorrectly applied to another twin's record. if existing_patient.pending_changes.any? existing_patient.pending_changes.merge!( existing_patient.slice(*auto_accepted_changes) ) existing_patient.restore_attributes(auto_accepted_changes) end end