Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions rails-app/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
class ApplicationController < ActionController::API
before_action :set_current_request_id

private

def set_current_request_id
Current.request_id = request.request_id
end
end
3 changes: 3 additions & 0 deletions rails-app/app/models/current.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Current < ActiveSupport::CurrentAttributes
attribute :request_id
end
22 changes: 10 additions & 12 deletions rails-app/app/models/veteran/disability_rating_response.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
module Veteran
class DisabilityRatingResponse
attr_accessor :combined_disability_rating, :combined_effective_date, :legal_effective_date,
:earliest_rating_end_date, :raw_data, :data_source, :metadata
attr_accessor :raw_data, :data_source, :metadata,
:total_disability_status, :total_disability_status_effective_date

def initialize(params = {})
@combined_disability_rating = params[:combinedDisabilityRating]
@combined_effective_date = params[:combinedEffectiveDate]
@legal_effective_date = params[:legalEffectiveDate]
@earliest_rating_end_date = params[:earliestRatingEndDate]
@raw_data = params[:rawData]
@data_source = params[:dataSource]
@metadata = params[:metadata]
@total_disability_status = params[:totalDisabilityStatus]
@total_disability_status_effective_date = params[:totalDisabilityStatusEffectiveDate]
@permanent_and_total_disability_status = params[:permanentAndTotalDisabilityStatus]
@permanent_and_total_disability_pension_award_status = params[:permanentAndTotalDisabilityPensionAwardStatus]
end

def as_json(options = {})
{
combinedDisabilityRating: combined_disability_rating,
combinedEffectiveDate: combined_effective_date,
legalEffectiveDate: legal_effective_date,
earliestRatingEndDate: earliest_rating_end_date,
rawData: raw_data,
totalDisabilityStatus: total_disability_status,
totalDisabilityStatusEffectiveDate: total_disability_status_effective_date,
dataSource: data_source,
metadata: metadata
metadata: metadata,
rawData: raw_data
}
end
end
Expand Down
8 changes: 5 additions & 3 deletions rails-app/app/services/reporting/report_data.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
module Reporting
class ReportData
attr_accessor :timestamp, :endpoint, :data_source, :client_id, :status_code, :success
attr_accessor :timestamp, :endpoint, :data_source, :client_id, :status_code, :success, :transaction_id

def initialize(timestamp:, endpoint:, data_source:, client_id:, status_code:, success:)
def initialize(timestamp:, endpoint:, data_source:, client_id:, status_code:, success:, transaction_id: Current.request_id)
@timestamp = timestamp
@endpoint = endpoint
@data_source = data_source
@client_id = client_id
@status_code = status_code
@success = success
@transaction_id = transaction_id
end

def to_json(*_args)
Expand All @@ -18,7 +19,8 @@ def to_json(*_args)
data_source: @data_source,
client_id: @client_id,
status_code: @status_code,
success: @success
success: @success,
transaction_id: @transaction_id
}.to_json
end
end
Expand Down
20 changes: 6 additions & 14 deletions rails-app/app/services/veteran/disability_rating_mapper.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
module Veteran
class DisabilityRatingMapper
def self.map_response(va_resp, datasource_duration, start_time)
attributes = va_resp.dig('data', 'attributes') || {}

individual_ratings = attributes['individual_ratings'] || []
earliest_end_date = individual_ratings.map { |r| r['rating_end_date'] }
.reject { |d| d.blank? }
.min

def self.map_response(total_disability_response, datasource_duration, start_time)
response_timestamp = Time.now

total_disabilty_data = total_disability_response.dig("data")
Veteran::DisabilityRatingResponse.new(
combinedDisabilityRating: attributes['combined_disability_rating'],
combinedEffectiveDate: attributes['combined_effective_date'],
legalEffectiveDate: attributes['legal_effective_date'],
earliestRatingEndDate: earliest_end_date,
rawData: va_resp,
totalDisabilityStatus: total_disabilty_data["total_disability"]["status"],
totalDisabilityStatusEffectiveDate: total_disabilty_data["total_disability"]["effective_date"],
rawData: total_disability_response,
dataSource: "VA",
metadata: {
apiVersion: ENV['SERVICE_VERSION'] || '1.3.0',
environment: ENV['ENVIRONMENT'] || 'development',
requestTimestamp: start_time.utc.iso8601(3),
responseTimestamp: response_timestamp.utc.iso8601(3),
datasourceDurationMillis: datasource_duration.to_i,
transactionId: SecureRandom.uuid
transactionId: Current.request_id || SecureRandom.uuid
}
)
end
Expand Down
52 changes: 31 additions & 21 deletions rails-app/app/services/veteran/va_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,60 @@ module Veteran
class NotFoundError < StandardError; end

class VaClient
DISABILITY_RATING_PATH = "/restricted/disability_rating".freeze
DISABILITY_RATING_SCOPE = "disability_rating_restricted.read".freeze
DISABILITY_RATING_PATH = "/disability_rating".freeze
RESTRICTED_DISABILITY_RATING_PATH = "/restricted/disability_rating".freeze
DISABILITY_RATING_SCOPE = "disability_rating_restricted.read disability_rating.read permanent_and_total_disability.read permanent_and_total_disability_restricted.read".freeze
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".freeze
TOTAL_DISABILITY_PATH = "/permanent_and_total_disability"
TOTAL_DISABILITY_RESTRICTED_PATH = "/restricted/permanent_and_total_disability"

def initialize
end


def lookup_disability_rating(req_params)
start_time = Time.now

rating_req = Veteran::DisabilityRatingRequest.new(req_params)
token = fetch_oauth_token
va_payload = rating_req.to_va_payload
rating_req = Veteran::DisabilityRatingRequest.new(req_params)

uri = URI(File.join(ENV['VA_BASE_URL'], DISABILITY_RATING_PATH))
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
total_disability_response = get_total_disability_response(rating_req, token)
datasource_duration = (Time.now - start_time) * 1000

Veteran::DisabilityRatingMapper.map_response(JSON.parse(total_disability_response.body), datasource_duration, start_time)
end

request = Net::HTTP::Post.new(uri.path, {
private
def get_total_disability_response(rating_req, token)
va_payload = rating_req.to_va_payload
if rating_req.can_use_restricted_endpoint?
total_disability_uri = URI(File.join(ENV['VA_BASE_URL'], TOTAL_DISABILITY_RESTRICTED_PATH))
else
total_disability_uri = URI(File.join(ENV['VA_BASE_URL'], TOTAL_DISABILITY_PATH))
end
http = Net::HTTP.new(total_disability_uri.host, total_disability_uri.port)
http.use_ssl = (total_disability_uri.scheme == 'https')

total_disability_request = Net::HTTP::Post.new(total_disability_uri.path, {
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => "Bearer #{token}"
})
request.body = va_payload.to_json

datasource_start_time = Time.now
response = http.request(request)
datasource_duration = (Time.now - datasource_start_time) * 1000
total_disability_request.body = va_payload.to_json

if response.code.to_i == 404
total_disability_response = http.request(total_disability_request)
if total_disability_response.code.to_i == 404
raise NotFoundError, "veteran not found"
end

if response.code.to_i < 200 || response.code.to_i >= 300
Rails.logger.error("VA disability rating failed: status=#{response.code} body=#{response.body.to_s[0..800]}")
raise "VA disability rating failed: status=#{response.code}"
if total_disability_response.code.to_i < 200 || total_disability_response.code.to_i >= 300
Rails.logger.error("VA disability rating failed: status=#{total_disability_response.code} body=#{total_disability_response.body.to_s[0..800]}")
raise "VA disability rating failed: status=#{total_disability_response.code}"
end

va_resp = JSON.parse(response.body)
Veteran::DisabilityRatingMapper.map_response(va_resp, datasource_duration, start_time)
return total_disability_response
end

private

def fetch_oauth_token
assertion = signed_client_assertion

Expand Down
39 changes: 0 additions & 39 deletions rails-app/test/services/education/nsc_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,44 +157,5 @@ def stub_nsc_requests(oauth_resp, submit_resp)
end
end
end

test 'circuit breaker trips after failures' do
# Mock Stoplight to simulate failure and then open circuit
# Since Stoplight seems to be a method provided by the gem, we access it via the module if possible
# or just skip this specific test if the environment is too weird.
# Let's try to find where Light is.
light_class = if defined?(::Stoplight::Light)
::Stoplight::Light
elsif defined?(Stoplight::Domain::Light)
# This doesn't seem right for configuration
nil
end

skip "Stoplight::Light not defined" unless light_class

old_data_store = light_class.default_data_store
light_class.default_data_store = Stoplight::DataStore::Memory.new

oauth_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
oauth_response.instance_variable_set(:@read, true)
oauth_response.instance_variable_set(:@body, { access_token: 'fake-token' }.to_json)
submit_response = Net::HTTPBadGateway.new('1.1', '502', 'Bad Gateway')

# Threshold is 3 by default. Let's trip it.
3.times do
stub_nsc_requests(oauth_response, submit_response) do
assert_raises(StandardError) { @coordinator.lookup_enrollment_status(@req_body) }
end
end

# 4th call should raise Stoplight::Error::RedLight without even trying Net::HTTP
assert_raises(Stoplight::Error::RedLight) do
@coordinator.lookup_enrollment_status(@req_body)
end
ensure
if defined?(light_class) && light_class && old_data_store
light_class.default_data_store = old_data_store
end
end
end
end
102 changes: 79 additions & 23 deletions rails-app/test/services/veteran/va_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class VaClientTest < ActiveSupport::TestCase
dateOfBirth: '1988-10-24',
ssn: '123456789'
}
Current.request_id = 'test-request-id'
end

teardown do
Expand All @@ -40,14 +41,22 @@ class VaClientTest < ActiveSupport::TestCase

private

def stub_va_requests(oauth_resp, va_resp)
def stub_va_requests(oauth_resp, va_total_disability_resp = nil, rating_path: nil, total_disability_path: nil)
http_mock_oauth = Minitest::Mock.new
http_mock_oauth.expect :use_ssl=, true, [true]
http_mock_oauth.expect :request, oauth_resp, [Net::HTTP::Post]

http_mock_va = Minitest::Mock.new
http_mock_va.expect :use_ssl=, true, [true]
http_mock_va.expect :request, va_resp, [Net::HTTP::Post]

if va_total_disability_resp
total_disability_matcher = ->(req) {
req.is_a?(Net::HTTP::Post) && (total_disability_path.nil? || req.path == total_disability_path)
}
http_mock_va.expect :request, va_total_disability_resp do |req|
total_disability_matcher.call(req)
end
end

calls = 0
Net::HTTP.stub :new, proc { |host, port|
Expand All @@ -63,14 +72,14 @@ def stub_va_requests(oauth_resp, va_resp)

public

test 'lookup_disability_rating success' do
test 'lookup_disability_rating success uses restricted endpoint when SSN is present' do
oauth_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
oauth_response.instance_variable_set(:@read, true)
oauth_response.instance_variable_set(:@body, { access_token: 'fake-token' }.to_json)

va_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
va_response.instance_variable_set(:@read, true)
va_response.instance_variable_set(:@body, {
va_rating_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
va_rating_response.instance_variable_set(:@read, true)
va_rating_response.instance_variable_set(:@body, {
data: {
attributes: {
combined_disability_rating: 100,
Expand All @@ -84,14 +93,71 @@ def stub_va_requests(oauth_resp, va_resp)
}
}.to_json)

stub_va_requests(oauth_response, va_response) do
result = @client.lookup_disability_rating(@req_params)
va_total_disability_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
va_total_disability_response.instance_variable_set(:@read, true)
va_total_disability_response.instance_variable_set(:@body, {
data: {
total_disability: {
status: true,
effective_date: "2023-01-01"
},
permanent_and_total: {
service_connected_status: false,
pension_award_status: false
}
}
}.to_json)

stub_va_requests(oauth_response, va_total_disability_response,
rating_path: '/va/restricted/disability_rating',
total_disability_path: '/va/restricted/permanent_and_total_disability') do
response = @client.lookup_disability_rating(@req_params)
assert_equal true, response.total_disability_status
assert_equal true, response.total_disability_status_effective_date.present?
assert_equal 'test-request-id', response.metadata[:transactionId]
end
end

test 'lookup_disability_rating success uses standard endpoint when SSN is absent' do
params_without_ssn = @req_params.except(:ssn)

oauth_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
oauth_response.instance_variable_set(:@read, true)
oauth_response.instance_variable_set(:@body, { access_token: 'fake-token' }.to_json)

va_rating_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
va_rating_response.instance_variable_set(:@read, true)
va_rating_response.instance_variable_set(:@body, {
data: {
attributes: {
combined_disability_rating: 70,
combined_effective_date: '2023-01-01',
legal_effective_date: '2023-01-01'
}
}
}.to_json)

assert_equal 100, result.combined_disability_rating
assert_equal '2023-01-01', result.combined_effective_date
assert_equal '2023-12-01', result.earliest_rating_end_date
assert_equal 'VA', result.data_source
assert_not_nil result.metadata
va_total_disability_response = Net::HTTPSuccess.new('1.1', '200', 'OK')
va_total_disability_response.instance_variable_set(:@read, true)
va_total_disability_response.instance_variable_set(:@body, {
data: {
total_disability: {
status: true,
effective_date: "2023-01-01"
},
permanent_and_total: {
service_connected_status: true,
pension_award_status: true
}
}
}.to_json)

stub_va_requests(oauth_response, va_total_disability_response,
rating_path: '/va/disability_rating',
total_disability_path: '/va/permanent_and_total_disability') do
response = @client.lookup_disability_rating(params_without_ssn)
assert_equal true, response.total_disability_status
assert_equal true, response.total_disability_status_effective_date.present?
end
end

Expand All @@ -108,15 +174,5 @@ def stub_va_requests(oauth_resp, va_resp)
end
end
end

test 'circuit breaker trips after failures' do
# In Stoplight 5.x, it's harder to mock the data store globally for just one test
# without affecting others or needing complex setup.
# Since the previous implementation skipped this if Stoplight::Light wasn't defined,
# and Stoplight 5.x doesn't define it at the top level, we'll keep the skip pattern
# but updated for the current version's reality if needed.

skip "Stoplight 5.x circuit breaker testing not implemented"
end
end
end