Class: DecisionReviewV1::Service

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

Overview

Proxy Service for the Lighthouse Decision Reviews API.

Constant Summary collapse

STATSD_KEY_PREFIX =
'api.decision_review'
ZIP_REGEX =
/^\d{5}(-\d{4})?$/
NO_ZIP_PLACEHOLDER =
'00000'

Constants included from Appeals::Helpers

Appeals::Helpers::DR_LOCKBOX

Class Method Summary collapse

Instance Method Summary collapse

Methods included from 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 Appeals::SupplementalClaimServices

#create_supplemental_claim, #get_supplemental_claim, #get_supplemental_claim_contestable_issues, #get_supplemental_claim_upload, #get_supplemental_claim_upload_url, #process_form4142_submission, #put_supplemental_claim_upload, #queue_form4142, #queue_submit_evidence_uploads, #save_form4142_submission, #submit_form4142

Methods included from Appeals::Helpers

#create_supplemental_claims_headers, #get_and_rejigger_required_info, #middle_initial, #payload_encrypted_string

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, backup_zip = nil) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/decision_review_v1/service.rb', line 303

def self.(user, backup_zip = nil)
  original_zip = user.postal_code.to_s
  backup_zip_from_frontend = backup_zip.to_s
  zip = if original_zip =~ ZIP_REGEX
          original_zip
        elsif backup_zip_from_frontend =~ ZIP_REGEX
          backup_zip_from_frontend
        else
          NO_ZIP_PLACEHOLDER
        end
  {
    'veteranFirstName' => transliterate_name(user.first_name),
    'veteranLastName' => transliterate_name(user.last_name),
    'zipCode' => zip,
    'fileNumber' => user.ssn.to_s.strip,
    'source' => 'va.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



129
130
131
# File 'lib/decision_review_v1/service.rb', line 129

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

Instance Method Details

#construct_tmpfile_name(appeal_submission_upload_id, original_filename) ⇒ Object



323
324
325
326
327
# File 'lib/decision_review_v1/service.rb', line 323

def construct_tmpfile_name(appeal_submission_upload_id, original_filename)
  return "appeal_submission_upload_#{appeal_submission_upload_id}_" if appeal_submission_upload_id.present?

  File.basename(original_filename, '.pdf').first(240)
end

#create_higher_level_review(request_body:, user:, version: 'V1') ⇒ 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)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/decision_review_v1/service.rb', line 37

def create_higher_level_review(request_body:, user:, version: 'V1')
  with_monitoring_and_error_handling do
    headers = create_higher_level_review_headers(user)
    common_log_params = { key: :overall_claim_submission, form_id: '996', user_uuid: user.uuid,
                          downstream_system: 'Lighthouse', params: { version: } }
    begin
      response = perform :post, 'higher_level_reviews', request_body, headers
      log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]'))
    rescue => e
      log_formatted(**common_log_params.merge(error_log_params(e)))
      raise e
    end
    raise_schema_error_unless_200_status response.status
    validate_against_schema json: response.body, schema: HLR_CREATE_RESPONSE_SCHEMA,
                            append_to_error_class: " (HLR_#{version}})"
    response
  end
end

#create_higher_level_review_headers(user) ⇒ Object (private)



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

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 = HLR_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)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/decision_review_v1/service.rb', line 140

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
      log_formatted(**common_log_params.merge(error_log_params(e)))
      raise e
    end
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body, schema: NOD_CREATE_RESPONSE_SCHEMA, append_to_error_class: ' (NOD_V1)'
    )
    response
  end
end

#create_notice_of_disagreement_headers(user) ⇒ Object (private)



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/decision_review_v1/service.rb', line 355

def create_notice_of_disagreement_headers(user)
  headers = {
    'X-VA-File-Number' => user.ssn.to_s.strip.presence,
    'X-VA-First-Name' => user.first_name.to_s.strip,
    'X-VA-Middle-Initial' => middle_initial(user),
    'X-VA-Last-Name' => user.last_name.to_s.strip.presence,
    'X-VA-Birth-Date' => user.birth_date.to_s.strip.presence,
    'X-VA-ICN' => user.icn.presence
  }.compact

  missing_required_fields = NOD_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

#error_log_params(error) ⇒ Object (private)



419
420
421
422
423
# File 'lib/decision_review_v1/service.rb', line 419

def error_log_params(error)
  log_params = { is_success: false, response_error: error }
  log_params[:body] = error.body if error.try(:status) == 422
  log_params
end

#get_contestable_issues_headers(user) ⇒ Object (private)



376
377
378
379
380
381
382
383
384
# File 'lib/decision_review_v1/service.rb', line 376

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-ICN' => user.icn.presence,
    '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)


62
63
64
65
66
67
68
69
70
# File 'lib/decision_review_v1/service.rb', line 62

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_V1)'
    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)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/decision_review_v1/service.rb', line 79

def get_higher_level_review_contestable_issues(user:, benefit_type:)
  with_monitoring_and_error_handling do
    path = "contestable_issues/higher_level_reviews?benefit_type=#{benefit_type}"
    headers = get_contestable_issues_headers(user)
    common_log_params = { key: :get_contestable_issues, form_id: '996', 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(error_log_params(e)))
      raise e
    end
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body,
      schema: GET_CONTESTABLE_ISSUES_RESPONSE_SCHEMA,
      append_to_error_class: ' (HLR_V1)'
    )
    response
  end
end

#get_legacy_appeals(user:) ⇒ Faraday::Response

Get Legacy Appeals for either a Higher-Level Review or a Supplemental Claim

Parameters:

  • user (User)

    Veteran who the form is in regard to

Returns:

  • (Faraday::Response)


110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/decision_review_v1/service.rb', line 110

def get_legacy_appeals(user:)
  with_monitoring_and_error_handling do
    path = 'legacy_appeals'
    headers = get_legacy_appeals_headers(user)
    response = perform :get, path, nil, headers
    raise_schema_error_unless_200_status response.status
    validate_against_schema(
      json: response.body,
      schema: GET_LEGACY_APPEALS_RESPONSE_SCHEMA,
      append_to_error_class: ' (DECISION_REVIEW_V1)'
    )
    response
  end
end

#get_legacy_appeals_headers(user) ⇒ Object (private)



386
387
388
389
390
391
392
393
# File 'lib/decision_review_v1/service.rb', line 386

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

  {
    'X-VA-SSN' => user.ssn.to_s,
    'X-VA-ICN' => user.icn.presence
  }
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)


170
171
172
173
174
175
176
177
178
179
# File 'lib/decision_review_v1/service.rb', line 170

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: NOD_SHOW_RESPONSE_SCHEMA, append_to_error_class: ' (NOD_V1)'
    )
    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)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/decision_review_v1/service.rb', line 187

def get_notice_of_disagreement_contestable_issues(user:)
  with_monitoring_and_error_handling do
    path = 'contestable_issues/notice_of_disagreements'
    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: GET_CONTESTABLE_ISSUES_RESPONSE_SCHEMA,
      append_to_error_class: ' (NOD_V1)'
    )
    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 returned from get_notice_of_disagreement_upload_url

Returns:

  • (Faraday::Response)


297
298
299
300
301
# File 'lib/decision_review_v1/service.rb', line 297

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:, file_number:, user_uuid: nil, appeal_submission_upload_id: nil) ⇒ 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

  • file_number (Integer)

    The file number or ssn

Returns:

  • (Faraday::Response)


209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/decision_review_v1/service.rb', line 209

def get_notice_of_disagreement_upload_url(nod_uuid:, file_number:, user_uuid: nil, appeal_submission_upload_id: nil) # rubocop:disable Metrics/MethodLength
  with_monitoring_and_error_handling do
    headers = { 'X-VA-File-Number' => file_number.to_s.strip.presence }
    common_log_params = {
      key: :get_lighthouse_evidence_upload_url,
      form_id: '10182',
      user_uuid:,
      upstream_system: 'Lighthouse',
      downstream_system: 'Lighthouse',
      params: {
        nod_uuid:,
        appeal_submission_upload_id:
      }
    }
    begin
      response = perform :post, 'notice_of_disagreements/evidence_submissions', { nod_uuid: }, headers
      log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: response.body))
      response
    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=v2
      log_formatted(**common_log_params.merge(error_log_params(e)))
      raise e
    end
  end
end

#handle_error(error:, message: nil) ⇒ Object (private)



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/decision_review_v1/service.rb', line 425

def handle_error(error:, message: nil)
  save_and_log_error(error:, message:)
  source_hash = { source: "#{error.class} raised in #{self.class}" }
  raise case error
        when Faraday::ParsingError
          DecisionReviewV1::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
            DecisionReviewV1::ServiceException.new(
              key: "DR_#{error.status}",
              response_values: source_hash,
              original_status: error.status,
              original_body: error.body
            )
          end
        else
          error
        end
end

#log_error_details(error:, message: nil) ⇒ Object (private)



410
411
412
413
414
415
416
417
# File 'lib/decision_review_v1/service.rb', line 410

def log_error_details(error:, message: nil)
  info = {
    message:,
    error_class: error.class,
    error:
  }
  ::Rails.logger.info(info)
end

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

Upload supporting evidence for a Notice of Disagreement

rubocop:disable Metrics/MethodLength

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_string (Hash)

    additional data

Returns:

  • (Faraday::Response)


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/decision_review_v1/service.rb', line 246

def put_notice_of_disagreement_upload(upload_url:, file_upload:, metadata_string:, user_uuid: nil, appeal_submission_upload_id: nil) # rubocop:disable Layout/LineLength
  tmpfile_name = construct_tmpfile_name(appeal_submission_upload_id, file_upload.filename)
  content_tmpfile = Tempfile.new([tmpfile_name, '.pdf'], 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) }
  common_log_params = {
    key: :evidence_upload_to_lighthouse,
    form_id: '10182',
    user_uuid:,
    downstream_system: 'Lighthouse',
    params: {
      upload_url:,
      appeal_submission_upload_id:
    }
  }
  with_monitoring_and_error_handling do
    response = perform :put, upload_url, params, { 'Content-Type' => 'multipart/form-data' }
    log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]'))
    response
  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=v2
    log_formatted(**common_log_params.merge(error_log_params(e)))
    raise e
  end
ensure
  content_tmpfile.close
  content_tmpfile.unlink
  json_tmpfile.close
  json_tmpfile.unlink
end

#raise_schema_error_unless_200_status(status) ⇒ Object (private)



469
470
471
472
473
# File 'lib/decision_review_v1/service.rb', line 469

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)



475
476
477
# File 'lib/decision_review_v1/service.rb', line 475

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

#save_and_log_error(error:, message:) ⇒ Object (private)



448
449
450
451
# File 'lib/decision_review_v1/service.rb', line 448

def save_and_log_error(error:, message:)
  save_error_details(error)
  log_error_details(error:, message:)
end

#save_error_details(error) ⇒ Object (private)



401
402
403
404
405
406
407
408
# File 'lib/decision_review_v1/service.rb', line 401

def save_error_details(error)
  PersonalInformationLog.create!(
    error_class: "#{self.class.name}#save_error_details exception #{error.class} (DECISION_REVIEW_V1)",
    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)



453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/decision_review_v1/service.rb', line 453

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)



395
396
397
398
399
# File 'lib/decision_review_v1/service.rb', line 395

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