Class: MedicalRecords::LighthouseClient

Inherits:
Common::Client::Base show all
Defined in:
lib/medical_records/lighthouse_client.rb

Overview

Core class responsible for Medical Records API interface operations with the Lighthouse FHIR server.

Instance Method Summary collapse

Methods inherited from Common::Client::Base

#config, configuration, #connection, #delete, #get, #perform, #post, #put, #raise_backend_exception, #raise_not_authenticated, #request, #sanitize_headers!, #service_name

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata

Constructor Details

#initialize(icn) ⇒ LighthouseClient

Initialize the client

Parameters:

  • icn (String)

    MHV patient ICN

Raises:



16
17
18
19
20
21
22
# File 'lib/medical_records/lighthouse_client.rb', line 16

def initialize(icn)
  super()

  raise Common::Exceptions::ParameterMissing, 'ICN' if icn.blank?

  @icn = icn
end

Instance Method Details

#authenticateObject



28
29
30
31
32
# File 'lib/medical_records/lighthouse_client.rb', line 28

def authenticate
  # FIXME: Explore doing this in a less janky way.
  # This is called by the MHV Controller Concern, but is not needed for this client
  # because it is handled in Lighthouse::VeteransHealth::Client::retrieve_bearer_token
end

#fetch_nested_value(object, field_path) ⇒ Object (protected)

Fetches the value of a potentially nested field from a given object.

Parameters:

  • object (Object)

    the object to fetch the value from

  • field_path (String)

    the dot-separated path to the field



164
165
166
167
168
# File 'lib/medical_records/lighthouse_client.rb', line 164

def fetch_nested_value(object, field_path)
  field_path.split('.').reduce(object) do |obj, method|
    obj.respond_to?(method) ? obj.send(method) : nil
  end
end

#get_allergy(allergy_id) ⇒ Object



48
49
50
51
# File 'lib/medical_records/lighthouse_client.rb', line 48

def get_allergy(allergy_id)
  bundle = lighthouse_client.get_allergy_intolerance(allergy_id)
  Oj.load(bundle[:body].to_json, symbol_keys: true)
end

#handle_api_errors(result) ⇒ Object (protected)



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/medical_records/lighthouse_client.rb', line 55

def handle_api_errors(result)
  if result.code.present? && result.code >= 400
    body = JSON.parse(result.body)
    diagnostics = body['issue']&.first&.fetch('diagnostics', nil)
    diagnostics = "Error fetching data#{": #{diagnostics}" if diagnostics}"

    # Special-case exception handling
    if result.code == 500 && diagnostics.include?('HAPI-1363')
      # "HAPI-1363: Either No patient or multiple patient found"
      raise MedicalRecords::PatientNotFound
    end

    # Default exception handling
    raise Common::Exceptions::BackendServiceException.new(
      "MEDICALRECORDS_#{result.code}",
      status: result.code,
      detail: diagnostics,
      source: self.class.to_s
    )
  end
end

#lighthouse_clientObject



24
25
26
# File 'lib/medical_records/lighthouse_client.rb', line 24

def lighthouse_client
  @lighthouse_client ||= Lighthouse::VeteransHealth::Client.new(@icn)
end

#list_allergiesObject



42
43
44
45
46
# File 'lib/medical_records/lighthouse_client.rb', line 42

def list_allergies
  bundle = lighthouse_client.list_allergy_intolerances
  bundle = Oj.load(bundle[:body].to_json, symbol_keys: true)
  sort_bundle(bundle, :recordedDate, :desc)
end

#list_vitals(from_date = nil, to_date = nil) ⇒ Object



34
35
36
37
38
39
40
# File 'lib/medical_records/lighthouse_client.rb', line 34

def list_vitals(from_date = nil, to_date = nil)
  params = { category: 'vital-signs' }
  params[:date] = ["ge#{from_date}", "le#{to_date}"] if from_date && to_date
  bundle = lighthouse_client.list_observations(params)
  bundle = Oj.load(bundle[:body].to_json, symbol_keys: true)
  sort_bundle(bundle, :recordedDate, :desc)
end

#measure_duration(event: 'default', tags: []) ⇒ Object (protected)



170
171
172
173
174
175
176
177
178
179
# File 'lib/medical_records/lighthouse_client.rb', line 170

def measure_duration(event: 'default', tags: [])
  # Use time since boot to avoid clock skew issues
  # https://github.com/sidekiq/sidekiq/issues/3999
  start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
  result = yield
  duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time).round(4)

  StatsD.measure("api.mhv.lighthouse.#{event}.duration", duration, tags:)
  result
end

#merge_bundles(bundle1, bundle2) ⇒ Object (protected)

Merge two FHIR bundles into one, with an updated total count.

Parameters:

  • bundle1 (FHIR:Bundle)

    The first FHIR bundle

  • bundle2 (FHIR:Bundle)

    The second FHIR bundle

  • page_num (FHIR:Bundle)


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/medical_records/lighthouse_client.rb', line 84

def merge_bundles(bundle1, bundle2)
  unless bundle1.resourceType == 'Bundle' && bundle2.resourceType == 'Bundle'
    raise 'Both inputs must be FHIR Bundles'
  end

  # Clone the first bundle to avoid modifying the original
  merged_bundle = bundle1.clone

  # Merge the entries from the second bundle into the merged_bundle
  merged_bundle.entry ||= []
  bundle2.entry&.each do |entry|
    merged_bundle.entry << entry
  end

  # Update the total count in the merged bundle
  merged_bundle.total = merged_bundle.entry.count

  merged_bundle
end

#paginate_bundle_entries(entries, page_size, page_num) ⇒ Object (protected)

Apply pagination to the entries in a FHIR::Bundle object. This assumes sorting has already taken place.

Parameters:

  • entries

    a list of FHIR objects

  • page_size (Fixnum)

    page size

  • page_num (Fixnum)

    which page to return



111
112
113
114
115
116
117
118
# File 'lib/medical_records/lighthouse_client.rb', line 111

def paginate_bundle_entries(entries, page_size, page_num)
  start_index = (page_num - 1) * page_size
  end_index = start_index + page_size
  paginated_entries = entries[start_index...end_index]

  # Return the paginated result or an empty array if no entries
  paginated_entries || []
end

#sort_bundle(bundle, field, order = :asc) ⇒ Object (protected)

Sort the FHIR::Bundle entries on a given field and sort order. If a field is not present, that entry is sorted to the end.

Parameters:

  • bundle (FHIR::Bundle)

    the bundle to sort

  • field (Symbol, String)

    the field to sort on (supports nested fields with dot notation)

  • order (Symbol) (defaults to: :asc)

    the sort order, :asc (default) or :desc



128
129
130
131
132
133
# File 'lib/medical_records/lighthouse_client.rb', line 128

def sort_bundle(bundle, field, order = :asc)
  field = field.to_s
  sort_bundle_with_criteria(bundle, order) do |resource|
    fetch_nested_value(resource, field)
  end
end

#sort_bundle_with_criteria(bundle, order = :asc) ⇒ Object (protected)

Sort the FHIR::Bundle entries based on a provided block. The block should handle different resource types and define how to extract the sorting value from each.

Parameters:

  • bundle (FHIR::Bundle)

    the bundle to sort

  • order (Symbol) (defaults to: :asc)

    the sort order, :asc (default) or :desc



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/medical_records/lighthouse_client.rb', line 142

def sort_bundle_with_criteria(bundle, order = :asc)
  sorted_entries = bundle[:entry].sort do |entry1, entry2|
    value1 = yield(entry1[:resource])
    value2 = yield(entry2[:resource])
    if value2.nil?
      -1
    elsif value1.nil?
      1
    else
      order == :asc ? value1 <=> value2 : value2 <=> value1
    end
  end
  bundle[:entry] = sorted_entries
  bundle
end