projectcypress/health-data-standards

View on GitHub
lib/health-data-standards/import/c32/patient_importer.rb

Summary

Maintainability
A
1 hr
Test Coverage
module HealthDataStandards
  module Import
    module C32

      # This class is the central location for taking a HITSP C32 XML document and converting it
      # into the processed form we store in MongoDB. The class does this by running each measure
      # independently on the XML document
      #
      # This class is a Singleton. It should be accessed by calling PatientImporter.instance
      class PatientImporter

        include Singleton
        include HealthDataStandards::Util
        include HealthDataStandards::Import::CDA::LocatableImportUtils

        # Creates a new PatientImporter with the following XPath expressions used to find content in 
        # a HITSP C32:
        #
        # Encounter entries
        #    //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.127']/cda:entry/cda:encounter
        #
        # Procedure entries
        #    //cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']
        #
        # Result entries - There seems to be some confusion around the correct templateId, so the code checks for both
        #    //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15.1'] | //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']
        #
        # Vital sign entries
        #    //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']
        #
        # Medication entries
        #    //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.112']/cda:entry/cda:substanceAdministration
        #
        # Codes for medications are found in the substanceAdministration with the following relative XPath
        #    ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
        #
        # Condition entries
        #    //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation
        #
        # Codes for conditions are determined by examining the value child element as opposed to the code child element
        #
        # Social History entries (non-C32 section, specified in the HL7 CCD)
        #    //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']
        #
        # Care Goal entries(non-C32 section, specified in the HL7 CCD)
        #    //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']
        #
        # Allergy entries
        #    //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.18']
        #
        # Immunization entries
        #    //cda:substanceAdministration[cda:templateId/@root='2.16.840.1.113883.10.20.1.24']
        #
        # Codes for immunizations are found in the substanceAdministration with the following relative XPath
        #    ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
        def initialize(check_usable = true)
          @section_importers = {}
          @section_importers[:encounters] = CDA::EncounterImporter.new
          @section_importers[:procedures] = CDA::ProcedureImporter.new
          @section_importers[:results] = CDA::ResultImporter.new
          @section_importers[:vital_signs] = CDA::VitalSignImporter.new
          @section_importers[:medications] = CDA::MedicationImporter.new
          @section_importers[:conditions] = ConditionImporter.new
          @section_importers[:social_history] = CDA::SectionImporter.new(CDA::EntryFinder.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']"))
          @section_importers[:care_goals] = CareGoalImporter.new
          @section_importers[:medical_equipment] = CDA::MedicalEquipmentImporter.new
          @section_importers[:allergies] = CDA::AllergyImporter.new
          @section_importers[:immunizations] = ImmunizationImporter.new
          @section_importers[:insurance_providers] = InsuranceProviderImporter.new
        end

        # @param [boolean] value for check_usable_entries...importer uses true, stats uses false 
        def check_usable(check_usable_entries)
          @section_importers.each_pair do |section, importer|
            importer.check_for_usable = check_usable_entries
          end
        end

        # Parses a HITSP C32 document and returns a Hash of of the patient.
        #
        # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
        #        will have the "cda" namespace registered to "urn:hl7-org:v3"
        # @return [Record] a Mongoid model representing the patient
        def parse_c32(doc)
          c32_patient = Record.new
          get_demographics(c32_patient, doc)
          create_c32_hash(c32_patient, doc)
          check_for_cause_of_death(c32_patient)
          
          c32_patient
        end
        
        # Checks the conditions to see if any of them have a cause of death set. If they do,
        # it will set the expired field on the Record. This is done here rather than replacing
        # the expried method on Record because other formats may actully tell you whether
        # a patient is dead or not.
        # @param [Record] c32_patient to check the conditions on and set the expired
        #               property if applicable
        def check_for_cause_of_death(c32_patient)
          cause_of_death = c32_patient.conditions.detect {|condition| condition.cause_of_death }
          if cause_of_death
            c32_patient.expired = true
            c32_patient.deathdate = cause_of_death.time_of_death
          end
        end

        # Create a simple representation of the patient from a HITSP C32
        # @param [Record] record Mongoid model to append the Entry objects to
        # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
        #        will have the "cda" namespace registered to "urn:hl7-org:v3"
        # @return [Hash] a represnetation of the patient with symbols as keys for each section
        def create_c32_hash(record, doc)
          nrh = CDA::NarrativeReferenceHandler.new
          nrh.build_id_map(doc)
          @section_importers.each_pair do |section, importer|
            record.send(section.to_setter, importer.create_entries(doc, nrh))
          end
        end

        # Inspects a C32 document and populates the patient Hash with first name, last name
        # birth date, gender and the effectiveTime.
        #
        # @param [Hash] patient A hash that is used to represent the patient
        # @param [Nokogiri::XML::Node] doc The C32 document parsed by Nokogiri
        def get_demographics(patient, doc)
          effective_date = doc.at_xpath('/cda:ClinicalDocument/cda:effectiveTime')['value']
          patient.effective_time = HL7Helper.timestamp_to_integer(effective_date)
          patient_role_element = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole')
          patient_element = patient_role_element.at_xpath('./cda:patient')
          patient.title = patient_element.at_xpath('cda:name/cda:title').try(:text)
          patient.first = patient_element.at_xpath('cda:name/cda:given').text
          patient.last = patient_element.at_xpath('cda:name/cda:family').text
          birthdate_in_hl7ts_node = patient_element.at_xpath('cda:birthTime')
          birthdate_in_hl7ts = birthdate_in_hl7ts_node['value']
          patient.birthdate = HL7Helper.timestamp_to_integer(birthdate_in_hl7ts)

          gender_node = patient_element.at_xpath('cda:administrativeGenderCode')
          patient.gender = gender_node['code']
          id_node = patient_role_element.at_xpath('./cda:id')
          patient.medical_record_number = id_node['extension']
          
          # parse race, ethnicity, and spoken language
          race_node = patient_element.at_xpath('cda:raceCode')
          patient.race = { code: race_node['code'], code_set: 'CDC-RE' } if race_node
          ethnicity_node = patient_element.at_xpath('cda:ethnicGroupCode')
          patient.ethnicity = {code: ethnicity_node['code'], code_set: 'CDC-RE'} if ethnicity_node
          marital_status_node = patient_element.at_xpath("./cda:maritalStatusCode")
          patient.marital_status = {code: marital_status_node['code'], code_set: "HL7 Marital Status"} if marital_status_node
          ra_node = patient_element.at_xpath("./cda:religiousAffiliationCode")
          patient.religious_affiliation = {code: ra_node['code'], code_set: "Religious Affiliation"} if ra_node
          languages = patient_element.search('languageCommunication').map {|lc| lc.at_xpath('cda:languageCode')['code'] }
          patient.languages = languages unless languages.empty?
          
          patient.addresses = patient_role_element.xpath("./cda:addr").map { |addr| import_address(addr) }
          patient.telecoms = patient_role_element.xpath("./cda:telecom").map { |tele| import_telecom(tele) }
          
        end
      end
    end
  end
end