Skip to content

Commit

Permalink
Feature/oh allergies (#19036)
Browse files Browse the repository at this point in the history
* Add OH allergies feature toggle

* add LH specfic client and config, use param to determine path

* Refactor LighthouseClient and LighthouseConfiguration

- Remove unnecessary code and comments in LighthouseClient
- Update base_path method in LighthouseClient to use the base URL from settings
- Clean up imports and remove unused middleware in LighthouseConfiguration
- Update token_request_url and client_id methods in LighthouseConfiguration to use values from settings
- Update key method in LighthouseConfiguration to use the private key from settings
- Update build method in LighthouseConfiguration to use the launch method for encoding the ICN
- Update lighthouse_headers method in LighthouseConfiguration to use symbol syntax for headers
- Update get_token method in LighthouseConfiguration to parse the access_token from the response body
- Remove unused include and skip_before_action in MrController
- Conditionally include and skip_before_action in MrController based on use_oh_data_path parameter
- Update client method in MrController to conditionally instantiate LighthouseClient or Client based on use_oh_data_path parameter

* Respect feature toggle

* Measure LH call durations

* clean up and set default count

* Add betamocks config

* Add betamocks config for lh patient health api

* Update mock variable format to match other LH services

* Update to remove assumptions around FHIR client

* Fetch individual allergy resources

* Update betamocks config

* Remove in progress code

* Restore comment

* Add spec wip

* Don't reinit allergies client

* Working version of lh allergies test

* Update lh client authenticate method

* Add better comment for lighthouse_client::authenticate

* Improve VH client handling

* Default new feature toggle to false in dev

* Add spec for show allergy (OH data path)

* Remove logging

* Update lib/medical_records/lighthouse_client.rb

* Initialize lh client in class method

---------

Co-authored-by: Stephen Barrs <stephen.barrs@va.gov>
  • Loading branch information
acrollet and stephenBarrs authored Nov 7, 2024
1 parent 2ec7ecc commit 3e0d6f8
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 3 deletions.
16 changes: 16 additions & 0 deletions config/betamocks/services_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,22 @@
:cache_multiple_responses:
:uid_location: "query"
:uid_locator: "icn"
# Patient Health API
- :method: :post
:path: "/oauth2/health/system/v1/token"
:file_path: "/lighthouse/veterans_health/token"
- :method: :get
:path: "/services/fhir/v0/r4/AllergyIntolerance/*"
:file_path: "/lighthouse/veterans_health/AllergyIntolerance/show"
:cache_multiple_responses:
:uid_location: url
:uid_locator: "\/services\/fhir\/v0\/r4\/AllergyIntolerance\/(.*)"
- :method: :get
:path: "/services/fhir/v0/r4/AllergyIntolerance"
:file_path: "/lighthouse/veterans_health/AllergyIntolerance/index"
:cache_multiple_responses:
:uid_location: query
:uid_locator: patient

# Lighthouse Benefits Documents API
- :name: "Lighthouse Benefits Documents"
Expand Down
4 changes: 4 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,10 @@ features:
actor_type: user
descriptiom: Enables access to MHV Account Creation API
enable_in_development: true
mhv_accelerated_delivery_allergies_enabled:
actor_type: user
description: Control fetching OH allergies data
enable_in_development: false
mhv_va_health_chat_enabled:
actor_type: user
description: Enables the VA Health Chat link at /my-health
Expand Down
1 change: 1 addition & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ lighthouse:
use_mocks: false
veterans_health:
url: https://sandbox-api.va.gov
use_mocks: false
fast_tracker:
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
api_scope:
Expand Down
3 changes: 3 additions & 0 deletions config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,14 @@ lighthouse:
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
api_scope:
- "launch"
- "patient/AllergyIntolerance.read"
- "patient/DiagnosticReport.read"
- "patient/Patient.read"
- "system/Patient.read"
- "patient/Observation.read"
- "patient/Practitioner.read"
- "patient/MedicationRequest.read"
- "patient/Condition.read"
aud_claim_url: "https://deptva-eval.okta.com/oauth2/aus8nm1q0f7VQ0a482p7/v1/token"
client_id: "fake_client_id"
grant_type: "client_credentials"
Expand Down
4 changes: 4 additions & 0 deletions lib/lighthouse/veterans_health/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def list_allergy_intolerances
get_list(first_response)
end

def get_allergy_intolerance(id)
perform_get("services/fhir/v0/r4/AllergyIntolerance/#{id}")
end

def list_bp_observations
params = {
category: 'vital-signs',
Expand Down
2 changes: 2 additions & 0 deletions lib/lighthouse/veterans_health/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def connection
# faraday.response(:logger, ::Logger.new(STDOUT), bodies: true) unless Rails.env.production?

faraday.response :json

faraday.response :betamocks if Settings.lighthouse.veterans_health.use_mocks
faraday.adapter Faraday.default_adapter
end
end
Expand Down
173 changes: 173 additions & 0 deletions lib/medical_records/lighthouse_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# frozen_string_literal: true

require 'common/client/base'
require 'lighthouse/veterans_health/client'

module MedicalRecords
##
# Core class responsible for Medical Records API interface operations with the Lighthouse FHIR server.
#
class LighthouseClient < Common::Client::Base
##
# Initialize the client
#
# @param icn [String] MHV patient ICN
#
def initialize(icn)
super()

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

@icn = icn
end

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

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

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

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

protected

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

##
# Merge two FHIR bundles into one, with an updated total count.
#
# @param bundle1 [FHIR:Bundle] The first FHIR bundle
# @param bundle2 [FHIR:Bundle] The second FHIR bundle
# @param page_num [FHIR:Bundle]
#
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

##
# Apply pagination to the entries in a FHIR::Bundle object. This assumes sorting has already taken place.
#
# @param entries a list of FHIR objects
# @param page_size [Fixnum] page size
# @param page_num [Fixnum] which page to return
#
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 the FHIR::Bundle entries on a given field and sort order. If a field is not present, that entry
# is sorted to the end.
#
# @param bundle [FHIR::Bundle] the bundle to sort
# @param field [Symbol, String] the field to sort on (supports nested fields with dot notation)
# @param order [Symbol] the sort order, :asc (default) or :desc
#
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 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.
#
# @param bundle [FHIR::Bundle] the bundle to sort
# @param order [Symbol] the sort order, :asc (default) or :desc
#
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

##
# Fetches the value of a potentially nested field from a given object.
#
# @param object [Object] the object to fetch the value from
# @param field_path [String] the dot-separated path to the field
#
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

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
end
end
14 changes: 12 additions & 2 deletions modules/my_health/app/controllers/my_health/mr_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'medical_records/client'
require 'medical_records/bb_internal/client'
require 'medical_records/phr_mgr/client'
require 'medical_records/lighthouse_client'

module MyHealth
class MrController < ApplicationController
Expand All @@ -19,8 +20,17 @@ class MrController < ApplicationController
protected

def client
@client ||= MedicalRecords::Client.new(session: { user_id: current_user.mhv_correlation_id,
icn: current_user.icn })
use_oh_data_path = Flipper.enabled?(:mhv_accelerated_delivery_allergies_enabled, @current_user) &&
params[:use_oh_data_path].to_i == 1
if @client.nil?
@client ||= if use_oh_data_path
MedicalRecords::LighthouseClient.new(current_user.icn)
else
MedicalRecords::Client.new(session: { user_id: current_user.mhv_correlation_id,
icn: current_user.icn })
end
end
@client
end

def phrmgr_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def index
end

def show
allergy_id = params[:id].try(:to_i)
allergy_id = params[:id].try(:strip)
resource = client.get_allergy(allergy_id)
render json: resource.to_json
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,50 @@
end
end
end

context 'Premium user when use_oh_data_path is true' do
let(:mhv_account_type) { 'Premium' }
let(:current_user) { build(:user, :mhv, va_patient:, mhv_account_type:, icn: '23000219') }

before do
sign_in_as(current_user)

allow(Flipper).to receive(:enabled?).with(:mhv_accelerated_delivery_allergies_enabled,
instance_of(User)).and_return(true)
allow(Flipper).to receive(:enabled?).with(:mhv_medical_records_new_eligibility_check).and_return(false)
end

it 'responds to GET #index' do
VCR.use_cassette('mr_client/get_a_list_of_allergies_oh_data_path') do
get '/my_health/v1/medical_records/allergies?use_oh_data_path=1'
end

expect(response).to be_successful
expect(response.body).to be_a(String)

body = JSON.parse(response.body)
expect(body['entry']).to be_an(Array)
expect(body['entry'].size).to be 2

item = body['entry'][1]
expect(item['resource']['resourceType']).to eq('AllergyIntolerance')
expect(item['resource']['category'][0]).to eq('food')
end

it 'responds to GET #show' do
allergy_id = '4-6Z8D6dAzABlkPZA'

VCR.use_cassette('mr_client/get_an_allergy_oh_data_path') do
get "/my_health/v1/medical_records/allergies/#{allergy_id}?use_oh_data_path=1"
end

expect(response).to be_successful
expect(response.body).to be_a(String)

body = JSON.parse(response.body)
expect(body['resourceType']).to eq('AllergyIntolerance')
expect(body['id']).to eq(allergy_id)
expect(body['category'][0]).to eq('food')
end
end
end
Loading

0 comments on commit 3e0d6f8

Please sign in to comment.