Class: DecisionReview::Service

Inherits:
Common::Client::Base show all
Includes:
Common::Client::Concerns::Monitoring, DecisionReviewV1::Appeals::LoggingUtils, SentryLogging
Defined in:
lib/decision_review/service.rb

Overview

Proxy Service for the Lighthouse Decision Reviews API.

Constant Summary collapse

STATSD_KEY_PREFIX =
'api.decision_review'
HLR_CREATE_RESPONSE_SCHEMA =
VetsJsonSchema::SCHEMAS.fetch 'HLR-CREATE-RESPONSE-200'
HLR_SHOW_RESPONSE_SCHEMA =
VetsJsonSchema::SCHEMAS.fetch 'HLR-SHOW-RESPONSE-200'
HLR_GET_CONTESTABLE_ISSUES_RESPONSE_SCHEMA =
VetsJsonSchema::SCHEMAS.fetch 'HLR-GET-CONTESTABLE-ISSUES-RESPONSE-200'
REQUIRED_CREATE_HEADERS =
%w[X-VA-First-Name X-VA-Last-Name X-VA-SSN X-VA-Birth-Date].freeze
NO_ZIP_PLACEHOLDER =
'00000'

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DecisionReviewV1::Appeals::LoggingUtils

#benchmark?, #benchmark_to_log_data_hash, #extract_uuid_from_central_mail_message, #log_formatted, #parse_form412_response_to_log_msg, #parse_lighthouse_response_to_log_msg, #run_and_benchmark_if_enabled

Methods included from Common::Client::Concerns::Monitoring

#increment, #increment_failure, #increment_total, #with_monitoring

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata

Methods inherited from Common::Client::Base

#config, configuration, #connection, #delete, #get, #perform, #post, #put, #raise_backend_exception, #raise_not_authenticated, #request, #sanitize_headers!, #service_name

Class Method Details

.file_upload_metadata(user) ⇒ Object



235
236
237
238
239
240
241
242
243
244
# File 'lib/decision_review/service.rb', line 235

def self.(user)
  {
    'veteranFirstName' => transliterate_name(user.first_name),
    'veteranLastName' => transliterate_name(user.last_name),
    'zipCode' => user.postal_code || NO_ZIP_PLACEHOLDER,
    'fileNumber' => user.ssn.to_s.strip,
    'source' => 'Vets.gov',
    'businessLine' => 'BVA'
  }.to_json
end

.transliterate_name(str) ⇒ Object

upstream requirements ^[a-zA-Z-/s]1,50$ Cannot be missing or empty or longer than 50 characters. Only upper/lower case letters, hyphens(-), spaces and forward-slash(/) allowed



250
251
252
# File 'lib/decision_review/service.rb', line 250

def self.transliterate_name(str)
  I18n.transliterate(str.to_s).gsub(%r{[^a-zA-Z\-/\s]}, '').strip.first(50)
end

Instance Method Details

#create_higher_level_review(request_body:, user:) ⇒ Faraday::Response

Create a Higher-Level Review

Parameters:

  • request_body (JSON)

    JSON serialized version of a Higher-Level Review Form (20-0996)

  • user (User)

    Veteran who the form is in regard to

Returns:

  • (Faraday::Response)


40
41
42
43
44
45
46
47
48
# File 'lib/decision_review/service.rb', line 40

def create_higher_level_review(request_body:, user:)
  with_monitoring_and_error_handling do
    headers = create_higher_level_review_headers(user)
    response = perform :post, 'higher_level_reviews', request_body, headers
    raise_schema_error_unless_200_status response.status
    validate_against_schema json: response.body, schema: HLR_CREATE_RESPONSE_SCHEMA, append_to_error_class: ' (HLR)'
    response
  end
end

#create_higher_level_review_headers(user) ⇒ Object (private)



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/decision_review/service.rb', line 256

def create_higher_level_review_headers(user)
  headers = {
    'X-VA-SSN' => user.ssn.to_s.strip.presence,
    'X-VA-ICN' => user.icn.presence,
    'X-VA-First-Name' => user.first_name.to_s.strip.first(12),
    'X-VA-Middle-Initial' => middle_initial(user),
    'X-VA-Last-Name' => user.last_name.to_s.strip.first(18).presence,
    'X-VA-Birth-Date' => user.birth_date.to_s.strip.presence,
    'X-VA-File-Number' => nil,
    'X-VA-Service-Number' => nil,
    'X-VA-Insurance-Policy-Number' => nil
  }.compact

  missing_required_fields = REQUIRED_CREATE_HEADERS - headers.keys
  if missing_required_fields.present?
    raise Common::Exceptions::Forbidden.new(
      source: "#{self.class}##{__method__}",
      detail: { missing_required_fields: }
    )
  end

  headers
end

#create_notice_of_disagreement(request_body:, user:) ⇒ Faraday::Response

Create a Notice of Disagreement

Parameters:

  • request_body (JSON)

    JSON serialized version of a Notice of Disagreement Form (10182)

  • user (User)

    Veteran who the form is in regard to

Returns:

  • (Faraday::Response)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/decision_review/service.rb', line 57

def create_notice_of_disagreement(request_body:, user:) # rubocop:disable Metrics/MethodLength
  with_monitoring_and_error_handling do
    headers = create_notice_of_disagreement_headers(user)
    common_log_params = {
      key: :overall_claim_submission,
      form_id: '10182',
      user_uuid: user.uuid,
      downstream_system: 'Lighthouse'
    }
    begin
      response = perform :post, 'notice_of_disagreements', request_body, headers
      log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]'))
    rescue => e
      # We can freely log Lighthouse's error responses because they do not include PII or PHI.
      # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v1.
      log_formatted(**common_log_params.merge(is_success: false, response_error: e))
      raise e
    end
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body, schema: Schemas::NOD_CREATE_RESPONSE_200, append_to_error_class: ' (NOD)'
    )
    response
  end
end

#create_notice_of_disagreement_headers(user) ⇒ Object (private)



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/decision_review/service.rb', line 280

def create_notice_of_disagreement_headers(user)
  headers = {
    'X-VA-First-Name' => user.first_name.to_s.strip, # can be an empty string for those with 1 legal name
    'X-VA-Middle-Initial' => middle_initial(user),
    'X-VA-Last-Name' => user.last_name.to_s.strip.presence,
    'X-VA-SSN' => user.ssn.to_s.strip.presence,
    'X-VA-ICN' => user.icn.presence,
    'X-VA-File-Number' => nil,
    'X-VA-Birth-Date' => user.birth_date.to_s.strip.presence
  }.compact

  missing_required_fields = REQUIRED_CREATE_HEADERS - headers.keys
  if missing_required_fields.present?
    raise Common::Exceptions::Forbidden.new(
      source: "#{self.class}##{__method__}",
      detail: { missing_required_fields: }
    )
  end

  headers
end

#get_contestable_issues_headers(user) ⇒ Object (private)



306
307
308
309
310
311
312
313
# File 'lib/decision_review/service.rb', line 306

def get_contestable_issues_headers(user)
  raise Common::Exceptions::Forbidden.new source: "#{self.class}##{__method__}" unless user.ssn

  {
    'X-VA-SSN' => user.ssn.to_s,
    'X-VA-Receipt-Date' => Time.zone.now.strftime('%Y-%m-%d')
  }
end

#get_higher_level_review(uuid) ⇒ Faraday::Response

Retrieve a Higher-Level Review

Parameters:

  • uuid (uuid)

    A Higher-Level Review’s UUID (included in a create_higher_level_review response)

Returns:

  • (Faraday::Response)


89
90
91
92
93
94
95
96
# File 'lib/decision_review/service.rb', line 89

def get_higher_level_review(uuid)
  with_monitoring_and_error_handling do
    response = perform :get, "higher_level_reviews/#{uuid}", nil
    raise_schema_error_unless_200_status response.status
    validate_against_schema json: response.body, schema: HLR_SHOW_RESPONSE_SCHEMA, append_to_error_class: ' (HLR)'
    response
  end
end

#get_higher_level_review_contestable_issues(user:, benefit_type:) ⇒ Faraday::Response

Get Contestable Issues for a Higher-Level Review

Parameters:

  • user (User)

    Veteran who the form is in regard to

  • benefit_type (String)

    Type of benefit the decision review is for

Returns:

  • (Faraday::Response)


122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/decision_review/service.rb', line 122

def get_higher_level_review_contestable_issues(user:, benefit_type:)
  with_monitoring_and_error_handling do
    path = "higher_level_reviews/contestable_issues/#{benefit_type}"
    headers = get_contestable_issues_headers(user)
    response = perform :get, path, nil, headers
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body,
      schema: HLR_GET_CONTESTABLE_ISSUES_RESPONSE_SCHEMA,
      append_to_error_class: ' (HLR)'
    )
    response
  end
end

#get_notice_of_disagreement(uuid) ⇒ Faraday::Response

Retrieve a Notice of Disagreement

Parameters:

  • uuid (uuid)

    A Notice of Disagreement’s UUID (included in a create_notice_of_disagreement response)

Returns:

  • (Faraday::Response)


104
105
106
107
108
109
110
111
112
113
# File 'lib/decision_review/service.rb', line 104

def get_notice_of_disagreement(uuid)
  with_monitoring_and_error_handling do
    response = perform :get, "notice_of_disagreements/#{uuid}", nil
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body, schema: Schemas::NOD_SHOW_RESPONSE_200, append_to_error_class: ' (NOD)'
    )
    response
  end
end

#get_notice_of_disagreement_contestable_issues(user:) ⇒ Faraday::Response

Get Contestable Issues for a Notice of Disagreement

Parameters:

  • user (User)

    Veteran who the form is in regard to

Returns:

  • (Faraday::Response)


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/decision_review/service.rb', line 143

def get_notice_of_disagreement_contestable_issues(user:) # rubocop:disable Metrics/MethodLength
  with_monitoring_and_error_handling do
    path = 'notice_of_disagreements/contestable_issues'
    headers = get_contestable_issues_headers(user)
    common_log_params = {
      key: :get_contestable_issues,
      form_id: '10182',
      user_uuid: user.uuid,
      upstream_system: 'Lighthouse'
    }
    begin
      response = perform :get, path, nil, headers
      log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]'))
    rescue => e
      # We can freely log Lighthouse's error responses because they do not include PII or PHI.
      # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v1.
      log_formatted(**common_log_params.merge(is_success: false, response_error: e))
      raise e
    end
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body,
      schema: Schemas::NOD_CONTESTABLE_ISSUES_RESPONSE_200,
      append_to_error_class: ' (NOD)'
    )
    response
  end
end

#get_notice_of_disagreement_upload(guid:) ⇒ Faraday::Response

Returns all of the data associated with a specific Notice of Disagreement Evidence Submission.

Parameters:

  • guid (uuid)

    the uuid returnd from get_notice_of_disagreement_upload_url

Returns:

  • (Faraday::Response)


229
230
231
232
233
# File 'lib/decision_review/service.rb', line 229

def get_notice_of_disagreement_upload(guid:)
  with_monitoring_and_error_handling do
    perform :get, "notice_of_disagreements/evidence_submissions/#{guid}", nil
  end
end

#get_notice_of_disagreement_upload_url(nod_uuid:, ssn:) ⇒ Faraday::Response

Get the url to upload supporting evidence for a Notice of Disagreement

Parameters:

  • nod_uuid (uuid)

    The uuid of the submited Notice of Disagreement

Returns:

  • (Faraday::Response)


179
180
181
182
183
184
# File 'lib/decision_review/service.rb', line 179

def get_notice_of_disagreement_upload_url(nod_uuid:, ssn:)
  with_monitoring_and_error_handling do
    perform :post, 'notice_of_disagreements/evidence_submissions', { nod_uuid: },
            { 'X-VA-SSN' => ssn.to_s.strip.presence }
  end
end

#handle_error(error) ⇒ Object (private)



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/decision_review/service.rb', line 330

def handle_error(error)
  save_error_details error
  source_hash = { source: "#{error.class} raised in #{self.class}" }

  raise case error
        when Faraday::ParsingError
          DecisionReview::ServiceException.new key: 'DR_502', response_values: source_hash
        when Common::Client::Errors::ClientError
          Sentry.set_extras body: error.body, status: error.status
          if error.status == 403
            Common::Exceptions::Forbidden.new source_hash
          else
            DecisionReview::ServiceException.new(
              key: "DR_#{error.status}",
              response_values: source_hash,
              original_status: error.status,
              original_body: error.body
            )
          end
        else
          error
        end
end

#middle_initial(user) ⇒ Object (private)



302
303
304
# File 'lib/decision_review/service.rb', line 302

def middle_initial(user)
  user.middle_name.to_s.strip.presence&.first&.upcase
end

#put_notice_of_disagreement_upload(upload_url:, file_upload:, metadata_string:) ⇒ Faraday::Response

Get the url to upload supporting evidence for a Notice of Disagreement

Parameters:

  • upload_url (String)

    The url for the document to be uploaded

  • file_path (String)

    The file path for the document to be uploaded

  • metadata (Hash)

    additional data

Returns:

  • (Faraday::Response)


196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/decision_review/service.rb', line 196

def put_notice_of_disagreement_upload(upload_url:, file_upload:, metadata_string:)
  content_tmpfile = Tempfile.new(file_upload.filename, encoding: file_upload.read.encoding)
  content_tmpfile.write(file_upload.read)
  content_tmpfile.rewind

  json_tmpfile = Tempfile.new('metadata.json', encoding: 'utf-8')
  json_tmpfile.write()
  json_tmpfile.rewind

  params = { metadata: Faraday::UploadIO.new(json_tmpfile.path, Mime[:json].to_s, 'metadata.json'),
             content: Faraday::UploadIO.new(content_tmpfile.path, Mime[:pdf].to_s, file_upload.filename) }

  # when we upgrade to Faraday >1.0
  # params = { metadata: Faraday::FilePart.new(json_tmpfile, Mime[:json].to_s, 'metadata.json'),
  #            content: Faraday::FilePart.new(content_tmpfile, Mime[:pdf].to_s, file_upload.filename) }
  with_monitoring_and_error_handling do
    perform :put, upload_url, params, { 'Content-Type' => 'multipart/form-data' }
  end
ensure
  content_tmpfile.close
  content_tmpfile.unlink
  json_tmpfile.close
  json_tmpfile.unlink
end

#raise_schema_error_unless_200_status(status) ⇒ Object (private)



370
371
372
373
374
# File 'lib/decision_review/service.rb', line 370

def raise_schema_error_unless_200_status(status)
  return if status == 200

  raise Common::Exceptions::SchemaValidationErrors, ["expecting 200 status received #{status}"]
end

#remove_pii_from_json_schemer_errors(errors) ⇒ Object (private)



376
377
378
# File 'lib/decision_review/service.rb', line 376

def remove_pii_from_json_schemer_errors(errors)
  errors.map { |error| error.slice 'data_pointer', 'schema', 'root_schema' }
end

#save_error_details(error) ⇒ Object (private)



321
322
323
324
325
326
327
328
# File 'lib/decision_review/service.rb', line 321

def save_error_details(error)
  PersonalInformationLog.create!(
    error_class: "#{self.class.name}#save_error_details exception #{error.class} (HLR) (NOD)",
    data: { error: Class.new.include(FailedRequestLoggable).exception_hash(error) }
  )
  Sentry.set_tags(external_service: self.class.to_s.underscore)
  Sentry.set_extras(url: config.base_path, message: error.message)
end

#validate_against_schema(json:, schema:, append_to_error_class: '') ⇒ Object (private)



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/decision_review/service.rb', line 354

def validate_against_schema(json:, schema:, append_to_error_class: '')
  errors = JSONSchemer.schema(schema).validate(json).to_a
  return if errors.empty?

  raise Common::Exceptions::SchemaValidationErrors, remove_pii_from_json_schemer_errors(errors)
rescue => e
  PersonalInformationLog.create!(
    error_class: "#{self.class.name}#validate_against_schema exception #{e.class}#{append_to_error_class}",
    data: {
      json:, schema:, errors:,
      error: Class.new.include(FailedRequestLoggable).exception_hash(e)
    }
  )
  raise
end

#with_monitoring_and_error_handlingObject (private)



315
316
317
318
319
# File 'lib/decision_review/service.rb', line 315

def with_monitoring_and_error_handling(&)
  with_monitoring(2, &)
rescue => e
  handle_error(e)
end