Class: BenefitsIntakeRemediationStatusJob

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job
Defined in:
app/sidekiq/benefits_intake_remediation_status_job.rb

Overview

Reporting job for Lighthouse Benefit Intake Failures

Constant Summary collapse

STATS_KEY =

metrics key

'api.benefits_intake.remediation_status'
BATCH_SIZE =

job batch size

Settings.lighthouse.benefits_intake.report.batch_size || 1000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(batch_size: BATCH_SIZE) ⇒ BenefitsIntakeRemediationStatusJob

create an instance



19
20
21
22
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 19

def initialize(batch_size: BATCH_SIZE)
  @batch_size = batch_size
  @total_handled = 0
end

Instance Attribute Details

#batch_sizeObject (readonly, private)

Returns the value of attribute batch_size.



49
50
51
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 49

def batch_size
  @batch_size
end

#form_idObject (readonly, private)

Returns the value of attribute form_id.



49
50
51
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 49

def form_id
  @form_id
end

#total_handledObject (readonly, private)

Returns the value of attribute total_handled.



49
50
51
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 49

def total_handled
  @total_handled
end

Instance Method Details

#batch_process(failures) ⇒ Object (private)

perform a bulk_status check in Lighthouse to retrieve current statuses a processing error will abort the job (no retries)

Parameters:

  • failures (Array<FormSubmission>)

    submissions with only ‘failure’ statuses



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 72

def batch_process(failures)
  intake_service = BenefitsIntake::Service.new

  failures.each_slice(batch_size) do |batch|
    batch_uuids = batch.map { |submission| submission.latest_attempt&.benefits_intake_uuid }
    Rails.logger.info('BenefitsIntakeRemediationStatusJob processing batch', batch_uuids:)

    response = intake_service.bulk_status(uuids: batch_uuids)
    raise response.body unless response.success?

    next unless (data = response.body['data'])

    handle_response(data, batch)
  end
end

#handle_response(response_data, failure_batch) ⇒ Object (private)

process response from Lighthouse to update outstanding failures

Parameters:

  • response_date (Hash)

    Lighthouse Benefits Intake API response

  • failure_batch (Array<FormSubmission>)

    current batch being processed



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 93

def handle_response(response_data, failure_batch)
  response_data.each do |submission|
    uuid = submission['id']
    form_submission = failure_batch.find do |submission_from_db|
      submission_from_db.latest_attempt&.benefits_intake_uuid == uuid
    end
    form_submission.form_type

    form_submission_attempt = form_submission.form_submission_attempts.last

    # https://developer.va.gov/explore/api/benefits-intake/docs
    status = submission.dig('attributes', 'status')
    lighthouse_updated_at = submission.dig('attributes', 'updated_at')
    if status == 'vbms'
      # submission was successfully uploaded into a Veteran's eFolder within VBMS
      form_submission_attempt.update(lighthouse_updated_at:)
      form_submission_attempt.remediate!
    end

    @total_handled = total_handled + 1
  end
end

#outstanding_failures(submissions) ⇒ Object (private)

determine if a claim has an outstanding failure each claim can have multiple FormSubmission, which can have multiple FormSubmissionAttempt conflate these and search for a non-failure, which rejects the claim from the list

Parameters:



57
58
59
60
61
62
63
64
65
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 57

def outstanding_failures(submissions)
  failures = submissions.group_by(&:saved_claim_id)
  failures.map do |_claim_id, fs|
    fs.sort_by!(&:created_at)
    attempts = fs.map(&:form_submission_attempts).flatten.sort_by(&:created_at)
    not_failure = attempts.find { |att| att.aasm_state != 'failure' }
    not_failure ? nil : fs.last
  end.compact
end

#perform(form_id = nil) ⇒ Object

search all submissions for outstanding failures poll LH endpoint to see if status has changed (case if endpoint had an error initially) report stats on submissions, grouped by form-type



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 27

def perform(form_id = nil)
  Rails.logger.info('BenefitsIntakeRemediationStatusJob started')

  form_submissions = FormSubmission.includes(:form_submission_attempts)
  failures = outstanding_failures(form_submissions.all)

  # filter running this job to a specific form_id/form_type
  @form_id = form_id
  failures.select! { |f| f.form_type == form_id } if form_id

  batch_process(failures) unless failures.empty?

  submission_audit

  Rails.logger.info('BenefitsIntakeRemediationStatusJob ended', total_handled:)
rescue => e
  # catch and log, but not re-raise to avoid sidekiq exhaustion alerts
  Rails.logger.error('BenefitsIntakeRemediationStatusJob ERROR', class: self.class.name, message: e.message)
end

#submission_auditObject (private)

gather metrics - grouped by form type



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 118

def submission_audit
  # requery form_submissions in case there was an update
  form_submissions = FormSubmission.includes(:form_submission_attempts)
  form_submission_groups = form_submissions.all.group_by(&:form_type)

  form_submission_groups.each do |form_type, submissions|
    next if form_id && form_id != form_type

    fs_saved_claim_ids = submissions.map(&:saved_claim_id).uniq.compact
    next unless (earliest = fs_saved_claim_ids.min)

    claims = SavedClaim.where(form_id: form_type).where('id >= ?', earliest)
    claim_ids = claims.map(&:id).uniq

    unsubmitted = claim_ids - fs_saved_claim_ids
    orphaned = fs_saved_claim_ids - claim_ids

    failures = outstanding_failures(submissions)
    failures.map! do |fs|
      { claim_id: fs.saved_claim_id, uuid: fs.latest_attempt.benefits_intake_uuid,
        error_message: fs.latest_attempt.error_message }
    end

    submission_audit_metrics(form_type, unsubmitted, orphaned, failures)
  end
end

#submission_audit_metrics(form_type, unsubmitted, orphaned, failures) ⇒ Object (private)

report metrics

Parameters:

  • form_type (String)

    the saved_claim form id

  • unsubmitted (Array<Integer>)

    list of SavedClaim ids that do not have a FormSubmission record

  • orphaned (Array<Integer>)

    list of saved_claim_ids with a FormSubmission, but no SavedClaim

  • failures (Array<Hash>)

    list of outstanding failures (claim.id, benefits_intake_uuid, error_message)



151
152
153
154
155
156
157
# File 'app/sidekiq/benefits_intake_remediation_status_job.rb', line 151

def submission_audit_metrics(form_type, unsubmitted, orphaned, failures)
  audit_log = "BenefitsIntakeRemediationStatusJob submission audit #{form_type}"
  StatsD.gauge("#{STATS_KEY}.unsubmitted_claims", unsubmitted.length, tags: ["form_id:#{form_type}"])
  StatsD.gauge("#{STATS_KEY}.orphaned_submissions", orphaned.length, tags: ["form_id:#{form_type}"])
  StatsD.gauge("#{STATS_KEY}.outstanding_failures", failures.length, tags: ["form_id:#{form_type}"])
  Rails.logger.info(audit_log, form_id: form_type, unsubmitted:, orphaned:, failures:)
end