Skip to content

Commit 0359427

Browse files
authored
feat: Surface has_failed_stations in v2 prescriptions response meta (#27043)
* Surface has_failed_stations in v2 prescriptions response meta PrescriptionsAdapter#parse now always returns a Hash: { prescriptions: Array<Prescription>, metadata: Hash } The metadata hash includes :has_failed_stations (boolean), which is true when VistA reports a failedStationList or Oracle Health returns an OperationOutcome with error-severity issues. Controller changes: - v2 PrescriptionsController#index merges metadata into response opts - v2 show/list_refillable_prescriptions extract [:prescriptions] - Mobile v1 PrescriptionsController extracts [:prescriptions] Also adds VCR cassette for VistA partial-failure scenario and updates OpenAPI spec with has_failed_stations in PrescriptionList meta. * Address Copilot review: align VCR placeholder and improve OpenAPI description * Fix OpenAPI: make has_failed_stations optional, fix recently_requested type - Remove has_failed_stations from required array since V1 endpoints don't include it (only V2 does) - Change recently_requested items type from string to PrescriptionDetail ref to match actual response (both V1 and V2 return prescription objects) * Split PrescriptionList schema into V1/V2-specific schemas Replace the monolithic PrescriptionList OpenAPI schema with version-specific definitions that accurately reflect each endpoint: - V1PrescriptionList: includes updated_at, failed_station_list, and V1PrescriptionFilterCount (5 status keys) - V2PrescriptionList: includes has_failed_stations boolean and V2PrescriptionFilterCount (8 status keys) - Shared schemas: PrescriptionSortMeta, PaginationMeta, PaginationLinks - V1 list_refillable: inline schema with updated_at, failed_station_list, recently_requested (array of PrescriptionDetail) - V2 list_refillable: fixed recently_requested items type from object to PrescriptionDetail ref Addresses Copilot review feedback about V1/V2 meta shape divergence. * Split OpenAPI PrescriptionList into V1/V2 schemas; fail loud on nil body - Replace monolithic PrescriptionList.yml with version-specific schemas: - V1PrescriptionList.yml: updated_at, failed_station_list, 5 filter keys - V2PrescriptionList.yml: has_failed_stations boolean, 8 filter keys - Update openapi.yaml route refs to use V1/V2 schemas - Regenerate openapi_merged.yaml via redocly bundle - Change nil body guard from silent empty return to ArgumentError - Update specs to expect ArgumentError on nil input * Address review: fix misleading spec name, align VCR placeholder * Align oracle_health_failed? with OperationOutcomeDetector::ERROR_SEVERITIES
1 parent 537e557 commit 0359427

File tree

16 files changed

+750
-218
lines changed

16 files changed

+750
-218
lines changed

lib/unified_health_data/adapters/prescriptions_adapter.rb

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_relative 'oracle_health_prescription_adapter'
55
require_relative 'v2_status_mapping'
66
require_relative '../source_constants'
7+
require_relative '../operation_outcome_detector'
78

89
module UnifiedHealthData
910
module Adapters
@@ -16,8 +17,11 @@ def initialize(current_user = nil)
1617
@oracle_adapter = OracleHealthPrescriptionAdapter.new
1718
end
1819

20+
# @param body [Hash] The raw UHD response body
21+
# @param current_only [Boolean] When true, excludes discontinued/expired meds older than 180 days
22+
# @return [Hash] Hash with :prescriptions (Array) and :metadata (Hash) keys
1923
def parse(body, current_only: false)
20-
return [] if body.nil?
24+
raise ArgumentError, 'UHD returned an empty response body' if body.nil?
2125

2226
prescriptions = []
2327

@@ -37,7 +41,9 @@ def parse(body, current_only: false)
3741

3842
# Apply V2 status mapping to all prescriptions when Cerner pilot flag is enabled
3943
# This is the single point where V2 status mapping is applied for both VistA and Oracle Health
40-
apply_v2_status_mapping_if_enabled(prescriptions)
44+
prescriptions = apply_v2_status_mapping_if_enabled(prescriptions)
45+
46+
{ prescriptions:, metadata: { has_failed_stations: any_source_failed?(body) } }
4147
end
4248

4349
private
@@ -126,6 +132,38 @@ def apply_v2_status_mapping_if_enabled(prescriptions)
126132

127133
apply_v2_status_mapping_to_all(prescriptions)
128134
end
135+
136+
# Checks whether either data source reported a failure in the UHD response.
137+
# VistA: failedStationList is a non-empty string (comma-separated station numbers).
138+
# Oracle Health: entry array contains an OperationOutcome resource with error-severity issues.
139+
def any_source_failed?(body)
140+
vista_failed?(body) || oracle_health_failed?(body)
141+
end
142+
143+
def vista_failed?(body)
144+
body.dig(SourceConstants::VISTA, 'failedStationList').present?
145+
end
146+
147+
# Defense-in-depth: today the Client raises UpstreamPartialFailure for
148+
# error/fatal OperationOutcomes before the adapter runs, so this branch
149+
# is not reachable. We keep it aligned with OperationOutcomeDetector's
150+
# ERROR_SEVERITIES so the flag stays correct if that upstream flow changes.
151+
def oracle_health_failed?(body)
152+
oracle_data = body[SourceConstants::ORACLE_HEALTH]
153+
return false if oracle_data.blank?
154+
155+
entries = oracle_data['entry']
156+
return false unless entries.is_a?(Array) && entries.present?
157+
158+
entries.any? do |e|
159+
next false unless e.dig('resource', 'resourceType') == 'OperationOutcome'
160+
161+
issues = e.dig('resource', 'issue')
162+
issues.is_a?(Array) && issues.any? do |i|
163+
OperationOutcomeDetector::ERROR_SEVERITIES.include?(i['severity'])
164+
end
165+
end
166+
end
129167
end
130168
end
131169
end

lib/unified_health_data/service.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ def get_single_condition(condition_id)
9292
# @param current_only [Boolean] When true, applies filtering logic to exclude:
9393
# - Discontinued/expired medications older than 180 days
9494
# Defaults to false to return all prescriptions without filtering
95-
# @return [Array<UnifiedHealthData::Prescription>] Array of prescription objects
95+
# @return [Hash] Hash with :prescriptions (Array<UnifiedHealthData::Prescription>) and
96+
# :metadata (Hash including :has_failed_stations Boolean)
9697
def get_prescriptions(current_only: false)
9798
with_monitoring do
9899
start_date = default_start_date
@@ -101,16 +102,16 @@ def get_prescriptions(current_only: false)
101102
body = response.body
102103

103104
adapter = UnifiedHealthData::Adapters::PrescriptionsAdapter.new(@user)
104-
prescriptions = adapter.parse(body, current_only:)
105+
result = adapter.parse(body, current_only:)
105106

106107
Rails.logger.info(
107108
message: 'UHD prescriptions retrieved',
108-
total_prescriptions: prescriptions.size,
109+
total_prescriptions: result[:prescriptions].size,
109110
current_filtering_applied: current_only,
110111
service: 'unified_health_data'
111112
)
112113

113-
prescriptions
114+
result
114115
end
115116
end
116117

modules/mobile/app/controllers/mobile/v1/prescriptions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def oh_transition_filter
8686
end
8787

8888
def fetch_prescriptions
89-
unified_health_service.get_prescriptions(current_only: true)
89+
unified_health_service.get_prescriptions(current_only: true)[:prescriptions]
9090
end
9191

9292
def filtered_prescriptions(list)

modules/mobile/spec/requests/mobile/v1/health/prescriptions_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136

137137
before do
138138
allow_any_instance_of(UnifiedHealthData::Service).to receive(:get_prescriptions)
139-
.and_return([rx_va, rx_non_va])
139+
.and_return({ prescriptions: [rx_va, rx_non_va], metadata: { has_failed_stations: false } })
140140
end
141141

142142
it 'filters out Non-VA meds and sets hasNonVaMeds meta true' do
@@ -180,7 +180,7 @@
180180

181181
before do
182182
allow_any_instance_of(UnifiedHealthData::Service).to receive(:get_prescriptions)
183-
.and_return([rx_va1, rx_va2])
183+
.and_return({ prescriptions: [rx_va1, rx_va2], metadata: { has_failed_stations: false } })
184184
end
185185

186186
it 'does not set hasNonVaMeds meta flag' do

modules/my_health/app/controllers/my_health/v2/prescriptions_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ def refill
7070
def index
7171
return unless validate_feature_flag
7272

73-
prescriptions = service.get_prescriptions(current_only: false).compact
73+
result = service.get_prescriptions(current_only: false)
74+
prescriptions = result[:prescriptions].compact
75+
source_metadata = result[:metadata]
76+
7477
recently_requested = get_recently_requested_prescriptions(prescriptions)
7578
raw_data = prescriptions.dup
7679
prescriptions = resource_data_modifications(prescriptions).compact
@@ -79,6 +82,7 @@ def index
7982
prescriptions, sort_metadata = apply_filters_and_sorting(prescriptions)
8083

8184
records, options = build_response_data(prescriptions, filter_count, recently_requested, sort_metadata)
85+
options[:meta] = options[:meta].merge(source_metadata)
8286

8387
log_prescriptions_access
8488
render json: MyHealth::V2::PrescriptionDetailsSerializer.new(records, options)
@@ -89,7 +93,7 @@ def show
8993

9094
raise Common::Exceptions::ParameterMissing, 'station_number' if params[:station_number].blank?
9195

92-
prescriptions = service.get_prescriptions(current_only: false).compact
96+
prescriptions = service.get_prescriptions(current_only: false)[:prescriptions].compact
9397
prescription = prescriptions.find do |p|
9498
p.prescription_id.to_s == params[:id].to_s &&
9599
p.station_number.to_s == params[:station_number].to_s
@@ -103,7 +107,7 @@ def show
103107
def list_refillable_prescriptions
104108
return unless validate_feature_flag
105109

106-
prescriptions = service.get_prescriptions(current_only: false).compact
110+
prescriptions = service.get_prescriptions(current_only: false)[:prescriptions].compact
107111
recently_requested = get_recently_requested_prescriptions(prescriptions)
108112
refillable_prescriptions = filter_data_by_refill_and_renew(prescriptions)
109113

modules/my_health/docs/openapi.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,7 +1870,7 @@ paths:
18701870
content:
18711871
application/json:
18721872
schema:
1873-
$ref: ./schemas/PrescriptionList.yml
1873+
$ref: ./schemas/V1PrescriptionList.yml
18741874
description: OK
18751875
"401":
18761876
content:
@@ -1956,7 +1956,7 @@ paths:
19561956
content:
19571957
application/json:
19581958
schema:
1959-
$ref: ./schemas/PrescriptionList.yml
1959+
$ref: ./schemas/V1PrescriptionList.yml
19601960
description: OK
19611961
"401":
19621962
content:
@@ -2283,7 +2283,7 @@ paths:
22832283
content:
22842284
application/json:
22852285
schema:
2286-
$ref: ./schemas/PrescriptionList.yml
2286+
$ref: ./schemas/V2PrescriptionList.yml
22872287
description: OK
22882288
"401":
22892289
content:

0 commit comments

Comments
 (0)