Skip to content
20 changes: 14 additions & 6 deletions modules/vaos/app/models/vaos/v2/unified/va_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@
module VAOS
module V2
module Unified
# Represents a single VA schedulable clinic (clinic IEN) at a Lighthouse facility.
# +id+ is the clinic IEN. +location_id+ is the parent facility +unique_id+ used with VAOS
# clinics and slots APIs.
class VAProvider < BaseProvider
attr_accessor :location_id, :facility_type, :scheduling_config
attr_accessor :location_id, :facility_name, :facility_type

def initialize(attrs = {})
super
self.provider_type = 'va'
end

# Builds a VAProvider from a Lighthouse Facility object
# (FacilitiesApi::V2::Lighthouse::Facility)
def self.from_lighthouse_facility(facility)
##
# @param facility [FacilitiesApi::V2::Lighthouse::Facility] parent facility (address, geo, phone)
# @param clinic [OpenStruct, Hash] VAOS clinic payload from SystemsService#get_facility_clinics
#
def self.from_facility_and_clinic(facility, clinic)
clinic = clinic.to_h if clinic.is_a?(OpenStruct)

new(
id: facility.unique_id,
id: clinic[:id],
location_id: facility.unique_id,
name: facility.name,
name: clinic[:service_name],
facility_name: facility.name,
Comment thread
JunTaoLuo marked this conversation as resolved.
address: parse_lighthouse_address(facility.address),
phone: facility.phone&.dig('main'),
latitude: facility.lat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ def initialize(current_user)
end

##
# Checks patient eligibility for a specific service type at a VA facility
# selected from the unified provider list. Maps the referral's category of care
# from Lighthouse format to VAOS format, then checks whether the patient is
# eligible for direct scheduling.
# Checks patient eligibility for a specific service type at a VA facility.
# Maps the referral's category of care from Lighthouse format to VAOS format,
# then checks whether the patient is eligible for direct scheduling.
#
# @param va_provider [VAOS::V2::Unified::VAProvider] selected VA facility from provider search
# @param facility_id [String] VA facility identifier (Lighthouse +unique_id+, same as VAOS location)
# @param category_of_care [String] Lighthouse service type from the referral (e.g., 'primaryCare')
# @return [Hash] with :facility_id, :vaos_service_type, and :direct_eligible
#
def check_eligibility(va_provider, category_of_care)
facility_id = va_provider.location_id
def check_eligibility(facility_id:, category_of_care:)
vaos_service_type = ServiceTypeMapper.to_vaos(category_of_care)

if vaos_service_type.nil?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ def initialize(current_user)
end

##
# Searches for both VA facilities and EPS CC providers near the user's address,
# filtered by the referral's category of care. Pins the referral's matched CC
# provider at the top, then sorts remaining results by distance.
# Searches for VA clinics (via Lighthouse facilities + VAOS clinics) and EPS CC providers
# near the user's address, filtered by the referral's category of care. Pins the referral's
# matched CC provider at the top, then sorts remaining results by distance.
#
# @param referral [Object] A CCRA referral object with category_of_care, provider NPI, etc.
# @param radius [Integer] Search radius in miles (default: 25)
Expand Down Expand Up @@ -69,9 +69,7 @@ def fetch_va_providers(user_address, referral, radius, lh_client:)
per_page: 50
)

providers = facilities.map { |f| VAOS::V2::Unified::VAProvider.from_lighthouse_facility(f) }
providers.each { |p| assign_distance(p, user_address) }
filter_va_by_category_of_care(providers, referral.category_of_care)
fetch_providers_for_facilities(facilities, referral, user_address)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the UX flow and guessing at how this service is to be used, we'll want to produce a list of VA providers which are clinics not facilities. There really isn't any use for raw VA facilities so I'm updating the logic here to:

  1. check eligibility at each facility
  2. fetch clinics at each eligible facility

rescue => e
Rails.logger.error("#{log_prefix}: VA facility search failed",
{
Expand All @@ -84,6 +82,19 @@ def fetch_va_providers(user_address, referral, radius, lh_client:)
[]
end

def fetch_providers_for_facilities(facilities, referral, user_address)
matching_facilities = filter_supported_facilities(facilities, referral.category_of_care)
clinical_service = ServiceTypeMapper.to_vaos(referral.category_of_care)

matching_facilities.flat_map do |facility|
fetch_clinics_for_facility(facility, clinical_service).map do |clinic|
provider = VAProvider.from_facility_and_clinic(facility, clinic)
assign_distance(provider, user_address)
provider
end
end
Comment thread
JunTaoLuo marked this conversation as resolved.
end

def fetch_eps_providers(user_address, referral, radius, eps_client:)
providers = eps_client.search_by_location(
latitude: user_address.latitude,
Expand Down Expand Up @@ -127,18 +138,45 @@ def assign_distance(provider, user_address)
)
end

def filter_va_by_category_of_care(providers, category_of_care)
return providers if category_of_care.blank?
def filter_supported_facilities(facilities, category_of_care)
return facilities if category_of_care.blank?

normalized = category_of_care.to_s.downcase.gsub(/[\s_-]+/, '')
vaos_service_type = ServiceTypeMapper.to_vaos(category_of_care)
return facilities if vaos_service_type.nil?

providers.select do |provider|
provider.schedulable_services.any? do |svc|
svc.to_s.downcase.gsub(/[\s_-]+/, '') == normalized
end
facilities.select do |facility|
eligibility_service.check_eligibility(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I updated this section to use the eligibility checks instead since this is closer to the production behavior.

facility_id: facility.unique_id,
category_of_care:
)[:direct_eligible]
end
Comment thread
JunTaoLuo marked this conversation as resolved.
end

def fetch_clinics_for_facility(facility, clinical_service)
systems_service.get_facility_clinics(
location_id: facility.unique_id,
clinical_service:
)
rescue => e
Rails.logger.warn(
"#{log_prefix}: Clinic fetch failed for facility #{facility.unique_id}",
{
error_class: e.class.name,
clinical_service:,
user_uuid: @cached_user_uuid
}.compact
)
[]
end

def systems_service
@systems_service ||= VAOS::V2::SystemsService.new(current_user)
end

def eligibility_service
@eligibility_service ||= EligibilityService.new(current_user)
end

def combine_and_sort(va_providers, eps_providers, referral)
referral_provider, other_eps = partition_referral_provider(eps_providers, referral)

Expand Down
74 changes: 74 additions & 0 deletions modules/vaos/app/services/vaos/v2/unified/slot_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

module VAOS
module V2
module Unified
# Fetches appointment availability (slots) for unified scheduling: VA clinic providers via
# VAOS SystemsService, community care via EPS ProviderService. VA providers must include
# +location_id+ and +id+ (clinic IEN; see VAProvider).
class SlotAdapter
Comment thread
JunTaoLuo marked this conversation as resolved.
Outdated
def initialize(user)
@user = user
end

##
# @param provider [BaseProvider] VAProvider or EpsProvider
# @param start_dt [String] ISO8601 start of search window
# @param end_dt [String] ISO8601 end of search window
# @param clinical_service [String, nil] VAOS clinical service (required for VA)
# @param eps_opts [Hash] options for Eps::ProviderService#get_provider_slots
# @return [Array<BaseSlot>]
#
def slots_for(provider:, start_dt:, end_dt:, clinical_service: nil, eps_opts: {})
case provider
when VAProvider
fetch_va_slots(provider, start_dt, end_dt, clinical_service)
when EpsProvider
fetch_eps_slots(provider, eps_opts)
else
raise ArgumentError, "Unsupported provider type: #{provider.class.name}"
end
end

private

attr_reader :user

def fetch_va_slots(provider, start_dt, end_dt, clinical_service)
raise Common::Exceptions::ParameterMissing, 'service_type' if clinical_service.blank?

if provider.location_id.blank? || provider.id.blank?
raise Common::Exceptions::UnprocessableEntity.new(
detail: 'VA provider requires location_id and id (clinic IEN) for slot lookup'
)
end

raw = systems_service.get_available_slots(
location_id: provider.location_id,
clinic_id: provider.id,
clinical_service:,
provider_id: nil,
start_dt:,
end_dt:
)
Array(raw).map { |slot| VASlot.from_vaos_slot(slot, location_id: provider.location_id) }
end

def fetch_eps_slots(provider, eps_opts)
provider_id = provider.provider_service_id.presence || provider.id
response = eps_provider_service.get_provider_slots(provider_id, eps_opts)
slots = response&.slots
Array(slots).map { |slot| EpsSlot.from_eps_slot(slot) }
end

def systems_service
@systems_service ||= VAOS::V2::SystemsService.new(user)
end

def eps_provider_service
@eps_provider_service ||= Eps::ProviderService.new(user)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@

let(:va_provider) do
VAOS::V2::Unified::VAProvider.new(
id: '983',
id: '455',
location_id: '983',
name: 'Cheyenne VA Medical Center',
facility_name: 'Cheyenne VA Medical Center',
name: 'CHY UROLOGY',
address: { street1: '2360 E Pershing Blvd', city: 'Cheyenne', state: 'WY', zip: '82001' },
phone: '307-778-7550',
latitude: 41.1456,
Expand Down
74 changes: 45 additions & 29 deletions modules/vaos/spec/models/vaos/v2/unified/va_provider_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,15 @@
end
end

describe '.from_lighthouse_facility' do
describe '.from_facility_and_clinic' do
let(:facility) do
double(
'FacilitiesApi::V2::Lighthouse::Facility',
id: 'vha_983',
unique_id: '983',
name: 'Cheyenne VA Medical Center',
address: {
'physical' => {
'address1' => '2360 East Pershing Boulevard',
'address2' => 'Suite 100',
'address3' => 'Building A',
'city' => 'Cheyenne',
'state' => 'WY',
'zip' => '82001'
Expand All @@ -40,41 +37,60 @@
)
end

it 'maps Lighthouse facility fields to VaProvider' do
provider = described_class.from_lighthouse_facility(facility)
let(:clinic) do
{
id: '1014',
station_id: '983',
service_name: 'CHY AUDIOLOGY',
physical_location: 'Main building'
}
end

it 'sets id and name from the clinic payload and location_id from the facility' do
provider = described_class.from_facility_and_clinic(facility, clinic)

expect(provider.id).to eq('983')
expect(provider.id).to eq('1014')
expect(provider.location_id).to eq('983')
expect(provider.name).to eq('Cheyenne VA Medical Center')
expect(provider.facility_name).to eq('Cheyenne VA Medical Center')
expect(provider.name).to eq('CHY AUDIOLOGY')
expect(provider.provider_type).to eq('va')
expect(provider.latitude).to eq(41.1456)
expect(provider.longitude).to eq(-104.7892)
expect(provider.phone).to eq('307-778-7550')
expect(provider.distance_from_user).to be_nil
expect(provider.facility_type).to eq('va_health_facility')
expect(provider.schedulable_services).to eq(%w[primaryCare audiology])
expect(provider.address).to eq({
street1: '2360 East Pershing Boulevard',
street2: 'Suite 100',
street3: 'Building A',
city: 'Cheyenne',
state: 'WY',
zip: '82001'
})
end

it 'handles nil services gracefully' do
allow(facility).to receive(:services).and_return(nil)
provider = described_class.from_lighthouse_facility(facility)
it 'uses service_name for name' do
provider = described_class.from_facility_and_clinic(
facility,
clinic.merge(service_name: 'PODIATRY CLINIC')
)

expect(provider.schedulable_services).to eq([])
expect(provider.name).to eq('PODIATRY CLINIC')
end

it 'handles nil address gracefully' do
allow(facility).to receive(:address).and_return(nil)
provider = described_class.from_lighthouse_facility(facility)
it 'uses facility unique_id as location_id regardless of clinic station_id' do
satellite_clinic = clinic.merge(station_id: '983GC', id: '945')

provider = described_class.from_facility_and_clinic(facility, satellite_clinic)

expect(provider.location_id).to eq('983')
expect(provider.id).to eq('945')
end

expect(provider.address).to be_nil
it 'accepts OpenStruct clinic payloads' do
provider = described_class.from_facility_and_clinic(
facility,
OpenStruct.new(clinic)
)

expect(provider.id).to eq('1014')
expect(provider.name).to eq('CHY AUDIOLOGY')
end

it 'handles nil facility services gracefully' do
allow(facility).to receive(:services).and_return(nil)

provider = described_class.from_facility_and_clinic(facility, clinic)

expect(provider.schedulable_services).to eq([])
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

let(:va_provider) do
VAOS::V2::Unified::VAProvider.new(
id: '983',
id: '1081',
location_id: '983',
name: 'Cheyenne VA Medical Center',
address: { street1: '2360 E Pershing Blvd', city: 'Cheyenne', state: 'WY', zip: '82001' },
phone: '307-778-7550',
Expand Down Expand Up @@ -44,7 +45,7 @@
it 'serializes VA provider attributes' do
result = serializer.serialize([va_provider]).first

expect(result[:id]).to eq('983')
expect(result[:id]).to eq('1081')
expect(result[:attributes][:name]).to eq('Cheyenne VA Medical Center')
expect(result[:attributes][:providerType]).to eq('va')
expect(result[:attributes][:distanceInMiles]).to eq(3.2)
Expand All @@ -57,6 +58,8 @@
expect(result[:id]).to eq('9mN718pH')
expect(result[:attributes][:name]).to eq('Dr. Bones @ Melbourne Medical')
expect(result[:attributes][:providerType]).to eq('community_care')
expect(result[:attributes]).not_to have_key(:locationId)
expect(result[:attributes]).not_to have_key(:clinicId)
end

it 'marks the referral provider correctly' do
Expand Down
Loading
Loading