Skip to content

Commit 93142ec

Browse files
118645 append ids of active appts to referral details (#24665)
* remove has_appointments attribute add appointments attribute with empty array default value * update method to accept optional arg for referral number * update default value to be empty object * add uri encoding for request params * update call for referral appointment check to include referral number * enahance return with appointment data * update default value * update method documentation * add unit spec for missing provider scenario * fix for line length * add public method for fetching active vaos and eps appointments by referral * remove public method for fetching active appointments by referral, now handled in vaos appointment service * update details show action to use vaos appointment service for appending active appointment ids to response payload * update active appointments return to indicate discrepancy between sources * update appts fetch to return a list of all de-duped appointments for both VAOS and EPS * log and re-raise appointments fetch errors * add time metrics tracking * reverse ordering of active appointments collection * remove error message from log * re-add has_appointments attribute to referral details data model * remove state check for eps appt status normallization
1 parent d030639 commit 93142ec

File tree

16 files changed

+965
-261
lines changed

16 files changed

+965
-261
lines changed

modules/vaos/app/controllers/vaos/v2/referrals_controller.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,38 @@ def show
4040
response.uuid = referral_uuid
4141

4242
log_referral_provider_metrics(response)
43+
add_appointment_data_to_referral(response)
4344

4445
render json: Ccra::ReferralDetailSerializer.new(response)
4546
end
4647

4748
private
4849

50+
def add_appointment_data_to_referral(referral)
51+
result = appointments_service.get_active_appointments_for_referral(referral.referral_number)
52+
53+
eps_appointments = result[:EPS][:data]
54+
vaos_appointments = result[:VAOS][:data]
55+
56+
referral.appointments = {
57+
EPS: {
58+
data: eps_appointments.map { |appt| { id: appt[:id], status: appt[:status], start: appt[:start] } }
59+
},
60+
VAOS: {
61+
data: vaos_appointments.map { |appt| { id: appt[:id], status: appt[:status], start: appt[:start] } }
62+
}
63+
}
64+
65+
# Only set has_appointments to true if there are appointments with status "active"
66+
eps_has_active = eps_appointments.any? { |appt| appt[:status] == 'active' }
67+
vaos_has_active = vaos_appointments.any? { |appt| appt[:status] == 'active' }
68+
referral.has_appointments = eps_has_active || vaos_has_active
69+
end
70+
71+
def appointments_service
72+
@appointments_service ||= VAOS::V2::AppointmentsService.new(current_user)
73+
end
74+
4975
# Logs the count of referrals returned from CCRA
5076
#
5177
# @param referrals [Array<Ccra::ReferralListEntry>] The collection of referrals

modules/vaos/app/models/ccra/referral_detail.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@ class ReferralDetail
99
:provider_specialty, :provider_telephone, :treating_facility, :referral_number,
1010
:referring_facility_name,
1111
:referring_facility_phone, :referring_facility_code,
12-
:referring_facility_address, :has_appointments,
12+
:referring_facility_address,
1313
:referral_date, :station_id, :referral_consult_id,
1414
:treating_facility_name, :treating_facility_code, :treating_facility_phone,
1515
:treating_facility_address
16-
attr_accessor :uuid
16+
attr_accessor :uuid, :appointments, :has_appointments
1717

1818
##
1919
# Initializes a new instance of ReferralDetail.
2020
#
2121
# @param attributes [Hash] A hash containing the referral details from the CCRA response.
2222
# @option attributes [Hash] :referral The main referral data container.
2323
def initialize(attributes = {})
24+
@uuid = nil # Will be set by controller
25+
@appointments = {} # Will be populated by controller
26+
2427
return if attributes.blank?
2528

2629
@expiration_date = attributes[:referral_expiration_date]
@@ -30,8 +33,6 @@ def initialize(attributes = {})
3033
@referral_consult_id = attributes[:referral_consult_id]
3134
@referral_date = attributes[:referral_date]
3235
@station_id = attributes[:station_id]
33-
@uuid = nil # Will be set by controller
34-
@has_appointments = attributes[:appointments].present?
3536

3637
# Parse provider and facility info
3738
parse_referring_facility_info(attributes[:referring_facility_info])
@@ -73,6 +74,7 @@ def referral_attributes
7374
'provider_specialty' => @provider_specialty,
7475
'referral_number' => @referral_number,
7576
'has_appointments' => @has_appointments,
77+
'appointments' => @appointments,
7678
'referral_date' => @referral_date,
7779
'station_id' => @station_id,
7880
'referral_consult_id' => @referral_consult_id,
@@ -106,6 +108,7 @@ def assign_basic_attributes(hash)
106108
@referral_date = hash['referral_date']
107109
@station_id = hash['station_id']
108110
@has_appointments = hash['has_appointments']
111+
@appointments = hash['appointments'] || {}
109112
@referral_consult_id = hash['referral_consult_id']
110113
@uuid = hash['uuid']
111114
end

modules/vaos/app/serializers/ccra/referral_detail_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ReferralDetailSerializer
1616
attribute :referral_consult_id
1717
attribute :uuid
1818
attribute :has_appointments
19+
attribute :appointments
1920
attribute :referral_date
2021
attribute :station_id
2122

modules/vaos/app/services/eps/appointment_service.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@ def get_appointment(appointment_id:, retrieve_latest_details: false)
3232
##
3333
# Get appointments data from EPS
3434
#
35+
# @param referral_number [String] Optional referral number to filter appointments
3536
# @return [Array<Hash>] Array of appointment hashes from EPS
3637
#
37-
def get_appointments
38+
def get_appointments(referral_number: nil)
39+
params = { patientId: patient_id }
40+
params[:referralNumber] = referral_number if referral_number.present?
41+
42+
query_string = URI.encode_www_form(params)
43+
3844
with_monitoring do
39-
response = perform(:get, "/#{config.base_path}/appointments?patientId=#{patient_id}",
45+
response = perform(:get, "/#{config.base_path}/appointments?#{query_string}",
4046
{}, request_headers_with_correlation_id)
4147

4248
# Check for error field in successful responses using reusable helper

modules/vaos/app/services/vaos/v2/appointments_service.rb

Lines changed: 145 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,38 @@ def referral_appointment_already_exists?(referral_id, pagination_params = {})
160160
return { exists: true } if vaos_response[:data].any? { |appt| appt[:referral_id] == referral_id }
161161
end
162162

163-
eps_appointments = eps_appointments_service.get_appointments
163+
eps_appointments = eps_appointments_service.get_appointments(referral_number: referral_id)
164164

165165
# Filter out draft EPS appointments when checking referral usage
166166
non_draft_eps_appointments = eps_appointments&.reject { |appt| appt[:state] == 'draft' } || []
167-
{ exists: appointment_with_referral_exists?(non_draft_eps_appointments, referral_id) }
167+
{ exists: non_draft_eps_appointments.any? }
168+
end
169+
170+
##
171+
# Get appointments for a referral from both EPS and VAOS
172+
#
173+
# Returns appointments from both sources with normalized status (active/cancelled)
174+
# Deduplicates appointments within each source by start time + providerServiceId (EPS) or start time (VAOS)
175+
# Logs discrepancies when same start time has different status across sources
176+
#
177+
# @param referral_number [String] The referral number to search for
178+
# @return [Hash] Contains EPS and VAOS data: { EPS: { data: [...] }, VAOS: { data: [...] } }
179+
# @raise [BackendServiceException] If either EPS or VAOS fails
180+
#
181+
def get_active_appointments_for_referral(referral_number)
182+
start_time = Time.current
183+
eps_appointments = fetch_and_normalize_eps_appointments(referral_number)
184+
vaos_appointments = fetch_and_normalize_vaos_appointments(referral_number)
185+
186+
StatsD.histogram('vaos.get_active_appointments_for_referral.duration',
187+
(Time.current - start_time) * 1000)
188+
189+
log_status_discrepancies(eps_appointments, vaos_appointments, referral_number)
190+
191+
{
192+
EPS: { data: eps_appointments },
193+
VAOS: { data: vaos_appointments }
194+
}
168195
end
169196

170197
# rubocop:enable Metrics/MethodLength
@@ -371,6 +398,122 @@ def extract_facility_identifiers(appointments)
371398

372399
private
373400

401+
def fetch_and_normalize_eps_appointments(referral_number)
402+
raw_appointments = eps_appointments_service.get_appointments(referral_number:)
403+
filtered = raw_appointments.reject { |appt| appt[:state] == 'draft' }
404+
normalized = filtered.map do |appt|
405+
{
406+
id: appt[:id],
407+
status: normalize_eps_status(appt),
408+
start: appt.dig(:appointment_details, :start),
409+
provider_service_id: appt[:provider_service_id],
410+
last_retrieved: appt.dig(:appointment_details, :last_retrieved)
411+
}
412+
end
413+
414+
deduplicated = deduplicate_eps_appointments(normalized)
415+
deduplicated.sort_by { |appt| appt[:start] || '' }.reverse
416+
rescue Common::Exceptions::BackendServiceException => e
417+
log_fetch_error('EPS', referral_number, e.class.name.to_s)
418+
raise
419+
end
420+
421+
def fetch_and_normalize_vaos_appointments(referral_number)
422+
vaos_response = get_all_appointments({})
423+
check_vaos_response_for_failures(vaos_response, referral_number)
424+
process_vaos_appointments(vaos_response[:data], referral_number)
425+
rescue Common::Exceptions::BackendServiceException => e
426+
log_fetch_error('VAOS', referral_number, e.class.name.to_s)
427+
raise
428+
end
429+
430+
def check_vaos_response_for_failures(vaos_response, referral_number)
431+
return if vaos_response[:meta][:failures].blank?
432+
433+
log_fetch_error('VAOS', referral_number, vaos_response[:meta][:failures])
434+
raise Common::Exceptions::BackendServiceException.new('VAOS_502',
435+
{ detail: vaos_response[:meta][:failures].to_s })
436+
end
437+
438+
def process_vaos_appointments(appointments_data, referral_number)
439+
filtered = appointments_data.select { |appt| appt[:referral_id] == referral_number }
440+
normalized = filtered.map do |appt|
441+
{
442+
id: appt[:id],
443+
status: normalize_vaos_status(appt),
444+
start: appt[:start],
445+
created: appt[:created]
446+
}
447+
end
448+
449+
deduplicated = deduplicate_vaos_appointments(normalized)
450+
deduplicated.sort_by { |appt| appt[:start] || '' }.reverse
451+
end
452+
453+
def log_fetch_error(source, referral_number, error_details)
454+
masked_referral = "***#{referral_number.to_s.last(4)}"
455+
Rails.logger.error("Failed to fetch #{source} appointments for referral #{masked_referral}: #{error_details}")
456+
end
457+
458+
def normalize_eps_status(appointment)
459+
if appointment.dig(:appointment_details, :status) == 'cancelled'
460+
'cancelled'
461+
else
462+
'active'
463+
end
464+
end
465+
466+
def normalize_vaos_status(appointment)
467+
appointment[:status] == 'cancelled' ? 'cancelled' : 'active'
468+
end
469+
470+
def deduplicate_eps_appointments(appointments)
471+
grouped = appointments.group_by { |appt| [appt[:start], appt[:provider_service_id]] }
472+
473+
grouped.map do |_key, duplicates|
474+
next duplicates.first if duplicates.size == 1
475+
476+
active = duplicates.select { |appt| appt[:status] == 'active' }
477+
candidates = active.any? ? active : duplicates
478+
479+
# Choose most recent lastRetrieved
480+
candidates.max_by { |appt| appt[:last_retrieved] || '' }
481+
end
482+
end
483+
484+
def deduplicate_vaos_appointments(appointments)
485+
grouped = appointments.group_by { |appt| appt[:start] }
486+
487+
grouped.map do |_key, duplicates|
488+
next duplicates.first if duplicates.size == 1
489+
490+
active = duplicates.select { |appt| appt[:status] == 'active' }
491+
candidates = active.any? ? active : duplicates
492+
candidates.max_by { |appt| appt[:created] || '' }
493+
end
494+
end
495+
496+
def log_status_discrepancies(eps_appointments, vaos_appointments, referral_number)
497+
eps_by_start = eps_appointments.group_by { |appt| appt[:start] }
498+
vaos_by_start = vaos_appointments.group_by { |appt| appt[:start] }
499+
500+
common_start_times = eps_by_start.keys & vaos_by_start.keys
501+
502+
common_start_times.each do |start_time|
503+
eps_statuses = eps_by_start[start_time].map { |appt| appt[:status] }.uniq
504+
vaos_statuses = vaos_by_start[start_time].map { |appt| appt[:status] }.uniq
505+
506+
next if eps_statuses == vaos_statuses
507+
508+
masked_referral = referral_number&.last(4) || 'unknown'
509+
Rails.logger.warn('Appointment status discrepancy between EPS and VAOS',
510+
{ referral_ending_in: masked_referral,
511+
start_time:,
512+
eps_statuses:,
513+
vaos_statuses: })
514+
end
515+
end
516+
374517
# Fetches appointments and travel claims in parallel using Concurrent::Promises
375518
# @return [Array] Array containing [response, travel_claims_result]
376519
def fetch_appointments_and_claims_parallel(start_date, end_date, statuses, pagination_params, tp_client)
@@ -1477,18 +1620,6 @@ def handle_appointment_request_error(exception, caller_name, pagination_params)
14771620
})
14781621
}
14791622
end
1480-
1481-
##
1482-
# Checks if any appointment in the given list has a referral that matches the referral_id
1483-
#
1484-
# @param appointments [Array<Hash>] List of appointments to check
1485-
# @param referral_id [String] The referral ID to search for
1486-
# @return [Boolean] true if an appointment with matching referral exists, false otherwise
1487-
def appointment_with_referral_exists?(appointments, referral_id)
1488-
appointments.any? do |appt|
1489-
appt[:referral] && appt[:referral][:referral_number] == referral_id
1490-
end
1491-
end
14921623
end
14931624
end
14941625
end

0 commit comments

Comments
 (0)