From a4b04322c9f9d226e659f49e84d5955e99dc2b8f Mon Sep 17 00:00:00 2001 From: SamStuckey Date: Tue, 8 Oct 2024 13:33:02 -0600 Subject: [PATCH] general save --- app/models/form526_submission_remediation.rb | 3 +- app/sidekiq/form526_status_polling_job.rb | 21 +-- .../form526_submission_failure_email_job.rb | 125 +++++++++++++++ .../submit.rb | 17 +- .../form526_status_polling_job_spec.rb | 101 ++++++++---- ...rm526_submission_failure_email_job_spec.rb | 149 ++++++++++++++++++ 6 files changed, 344 insertions(+), 72 deletions(-) create mode 100644 app/sidekiq/form526_submission_failure_email_job.rb create mode 100644 spec/sidekiq/form526_submission_failure_email_job_spec.rb diff --git a/app/models/form526_submission_remediation.rb b/app/models/form526_submission_remediation.rb index 8f14a43e132..1833fad1bfd 100644 --- a/app/models/form526_submission_remediation.rb +++ b/app/models/form526_submission_remediation.rb @@ -11,7 +11,8 @@ class Form526SubmissionRemediation < ApplicationRecord STATSD_KEY_PREFIX = 'form526_submission_remediation' - enum remediation_type: { manual: 0, ignored_as_duplicate: 1, email_notification_to_vet: 2 } + # [wipn8923] keep + # enum remediation_type: { manual: 0, ignored_as_duplicate: 1, email_notification_to_vet: 2 } def mark_as_unsuccessful(context) self.success = false diff --git a/app/sidekiq/form526_status_polling_job.rb b/app/sidekiq/form526_status_polling_job.rb index a1a4f64a0a2..f3f48a40c3c 100644 --- a/app/sidekiq/form526_status_polling_job.rb +++ b/app/sidekiq/form526_status_polling_job.rb @@ -53,7 +53,7 @@ def handle_submission(status, form_submission) if %w[error expired].include? status log_result('failure') form_submission.rejected! - notify_veteran + Form526SubmissionFailureEmail.perform_async(form526_submission_id: form_submission.id) elsif status == 'vbms' log_result('true_success') form_submission.accepted! @@ -69,27 +69,8 @@ def handle_submission(status, form_submission) end end - def log_result(result) StatsD.increment("#{STATS_KEY}.526.#{result}") StatsD.increment("#{STATS_KEY}.all_forms.#{result}") end - - def notify_veteran - first_name = parsed_form.dig('veteranFullName', 'first') - # [wipn8923] form526_submission_failure_notification_template_id is not defined anywhere - Settings.vanotify.services.benefits_disability.template_id.form526_submission_failure_notification_template_id - api_key = Settings.vanotify.services.benefits_disability.api_key - salutation = first_name ? "Dear #{first_name}," : '' - - VANotify::EmailJob.perform_async( - email, - template_id, - { 'salutation' => salutation }, - api_key - ) - Form526SubmissionRemediation.create!(form526_submission_id:, - type: :failure_email, - lifecycle: ["failed with error: #{error_message}"]) - end end diff --git a/app/sidekiq/form526_submission_failure_email_job.rb b/app/sidekiq/form526_submission_failure_email_job.rb new file mode 100644 index 00000000000..6f3495e7565 --- /dev/null +++ b/app/sidekiq/form526_submission_failure_email_job.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'va_notify/service' + +# [wipn8923] new job +class Form526SubmissionFailureEmail + include Sidekiq::Job + + attr_reader :submission_id + + STATSD_METRIC_PREFIX = 'api.form_526.veteran_notifications.form526_submission_failure_email' + + sidekiq_options retry: 14 + + sidekiq_retries_exhausted do |msg, _ex| + job_id = msg['jid'] + error_class = msg['error_class'] + error_message = msg['error_message'] + form526_submission_id = msg['args'].first + + timestamp = Time.now.utc + + new_error = { + "#{timestamp.to_i}": { + caller_method: __method__.to_s, + timestamp:, + form526_submission_id: + } + } + + Rails.logger.warn( + 'Form526SubmissionFailureEmailJob retries exhausted', + { + job_id:, + timestamp:, + form526_submission_id:, + error_class:, + error_message: + } + ) + + StatsD.increment("#{STATSD_METRIC_PREFIX}.exhausted") + rescue => e + Rails.logger.error( + 'Failure in Form526SubmissionFailureEmailJob#sidekiq_retries_exhausted', + { + job_id:, + messaged_content: e.message, + submission_id: submission_id, + pre_exhaustion_failure: { + error_class:, + error_message: + } + } + ) + raise e + end + + def initialize(submission_id) + @submission_id = submission_id + end + + def perform + send_email + track_remedial_action + log_dispatch + rescue => e + log_failure(e) + raise + end + + private + + def submission + @submission ||= Form526Submission.find(submission_id) + end + + def send_email(submission) + email_client = VaNotify::Service.new(Settings.vanotify.services.benefits_disability.api_key) + template_id = Settings.vanotify.services.benefits_disability.template_id + .form526_submission_failure_notification_template_id + email_address = submission.veteran_email_address + first_name = submission.get_first_name + date_submitted = submission.format_creation_time_for_mailers + email_client.send_email( + email_address:, + template_id:, + personalisation: { + first_name:, + date_submitted: + } + ) + end + + def track_remedial_action + Form526SubmissionRemediation.create!(form526_submission: submission, + ignored_as_duplicate: true, + lifecycle: ["failed with error: #{error_message}"]) + end + + def log_dispatch(form526_submission_id) + Rails.logger.info( + 'Form526SubmissionFailureEmail notification dispatched', + { + form526_submission_id: submission.id, + timestamp: Time.now.utc + } + ) + + StatsD.increment("#{STATSD_METRIC_PREFIX}.success") + end + + def log_failure(error) + Rails.logger.info( + 'Form526SubmissionFailureEmail notification failed', + { + form526_submission_id: submission.id, + error_message: error.try(:message), + timestamp: Time.now.utc + } + ) + + StatsD.increment("#{STATSD_METRIC_PREFIX}.error") + end +end diff --git a/lib/sidekiq/form526_backup_submission_process/submit.rb b/lib/sidekiq/form526_backup_submission_process/submit.rb index c7bbff2f404..9e2a1b2f302 100644 --- a/lib/sidekiq/form526_backup_submission_process/submit.rb +++ b/lib/sidekiq/form526_backup_submission_process/submit.rb @@ -53,22 +53,7 @@ class Submit { job_id:, error_class:, error_message:, timestamp:, form526_submission_id: } ) - # [wipn8923] none of this has been checked yet - first_name = parsed_form.dig('veteranFullName', 'first') - # [wipn8923] form526_submission_failure_notification_template_id is not defined anywhere - Settings.vanotify.services.benefits_disability.template_id.form526_submission_failure_notification_template_id - api_key = Settings.vanotify.services.benefits_disability.api_key - salutation = first_name ? "Dear #{first_name}," : '' - - VANotify::EmailJob.perform_async( - email, - template_id, - { 'salutation' => salutation }, - api_key - ) - Form526SubmissionRemediation.create!(form526_submission_id:, - type: :failure_email, - lifecycle: ["failed with error: #{error_message}"]) + Form526SubmissionFailureEmail.perform_async(form526_submission_id: form_submission.id) rescue => e ::Rails.logger.error( 'Failure in Form526BackupSubmission#sidekiq_retries_exhausted', diff --git a/spec/sidekiq/form526_status_polling_job_spec.rb b/spec/sidekiq/form526_status_polling_job_spec.rb index dbe75a6db56..3ac516ff9ea 100644 --- a/spec/sidekiq/form526_status_polling_job_spec.rb +++ b/spec/sidekiq/form526_status_polling_job_spec.rb @@ -25,6 +25,41 @@ end context 'polling on pending submissions' do + let(:api_response) do + { + 'data' => [ + { + 'id' => backup_submission_a.backup_submitted_claim_id, + 'attributes' => { + 'guid' => backup_submission_a.backup_submitted_claim_id, + 'status' => 'vbms' + } + }, + { + 'id' => backup_submission_b.backup_submitted_claim_id, + 'attributes' => { + 'guid' => backup_submission_b.backup_submitted_claim_id, + 'status' => 'success' + } + }, + { + 'id' => backup_submission_c.backup_submitted_claim_id, + 'attributes' => { + 'guid' => backup_submission_c.backup_submitted_claim_id, + 'status' => 'error' + } + }, + { + 'id' => backup_submission_d.backup_submitted_claim_id, + 'attributes' => { + 'guid' => backup_submission_d.backup_submitted_claim_id, + 'status' => 'expired' + } + } + ] + } + end + describe 'submission to the bulk status report endpoint' do it 'submits only pending form submissions' do pending_claim_ids = Form526Submission.pending_backup.pluck(:backup_submitted_claim_id) @@ -84,41 +119,6 @@ end describe 'updating the form 526s local submission state' do - let(:api_response) do - { - 'data' => [ - { - 'id' => backup_submission_a.backup_submitted_claim_id, - 'attributes' => { - 'guid' => backup_submission_a.backup_submitted_claim_id, - 'status' => 'vbms' - } - }, - { - 'id' => backup_submission_b.backup_submitted_claim_id, - 'attributes' => { - 'guid' => backup_submission_b.backup_submitted_claim_id, - 'status' => 'success' - } - }, - { - 'id' => backup_submission_c.backup_submitted_claim_id, - 'attributes' => { - 'guid' => backup_submission_c.backup_submitted_claim_id, - 'status' => 'error' - } - }, - { - 'id' => backup_submission_d.backup_submitted_claim_id, - 'attributes' => { - 'guid' => backup_submission_d.backup_submitted_claim_id, - 'status' => 'expired' - } - } - ] - } - end - it 'updates local state to reflect the returned statuses' do pending_claim_ids = Form526Submission.pending_backup .pluck(:backup_submitted_claim_id) @@ -138,6 +138,37 @@ expect(backup_submission_d.reload.backup_submitted_claim_status).to eq 'rejected' end end + + # [wipn8923] new spec (polling) + context 'when a failure type response is returned from the API' do + describe 'creating and tracking notifications' do + it 'notifies the veteran and marks the submission as remediated' do + pending_claim_ids = Form526Submission.pending_backup + .pluck(:backup_submitted_claim_id) + response = double + + allow(response).to receive(:body).and_return(api_response) + allow_any_instance_of(BenefitsIntakeService::Service) + .to receive(:get_bulk_status_of_uploads) + .with(pending_claim_ids) + .and_return(response) + + email_params = {} + # allow_any_instance_of(VANotify::EmailJob) + # .to receive(:perform_async) + # .with(email_params) + + Form526StatusPollingJob.new.perform + + # expect_any_instance_of(VANotify::EmailJob).to have_received(:perform) + + expect(backup_submission_a.reload.remediated?).to eq false + expect(backup_submission_b.reload.remediated?).to eq false + expect(backup_submission_c.reload.remediated?).to eq true + expect(backup_submission_d.reload.remediated?).to eq true + end + end + end end end end diff --git a/spec/sidekiq/form526_submission_failure_email_job_spec.rb b/spec/sidekiq/form526_submission_failure_email_job_spec.rb new file mode 100644 index 00000000000..b1381bc50b7 --- /dev/null +++ b/spec/sidekiq/form526_submission_failure_email_job_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# [wipn8923] new spec +RSpec.describe Form526SubmissionFailureEmail, type: :job do + subject { described_class } + + let!(:form526_submission) do + create( + :form526_submission, + :with_uploads + ) + end + + let(:upload_data) { [form526_submission.form[Form526Submission::FORM_526_UPLOADS].first] } + let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/files/sm_file1.jpg', 'image/jpg') } + let!(:form_attachment) do + sea = SupportingEvidenceAttachment.new( + guid: upload_data.first['confirmationCode'] + ) + + sea.set_file_data!(file) + sea.save! + sea + end + + let(:notification_client) { double('Notifications::Client') } + + before do + Sidekiq::Job.clear_all + allow(Notifications::Client).to receive(:new).and_return(notification_client) + end + + describe '#perform' do + let(:formatted_submit_date) do + # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" + form526_submission.created_at.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') + end + + let(:obscured_filename) { 'sm_XXXe1.jpg' } + + it 'dispatches a failure notification email with an obscured filename' do + expect(notification_client).to receive(:send_email).with( + # Email address and first_name are from our User fixtures + # form526_document_upload_failure_notification_template_id is a placeholder in settings.yml + { + email_address: 'test@email.com', + template_id: 'form526_document_upload_failure_notification_template_id', + personalisation: { + first_name: 'BEYONCE', + filename: obscured_filename, + date_submitted: formatted_submit_date + } + } + ) + + subject.perform_async(form526_submission.id, form_attachment.guid) + subject.drain + end + + describe 'logging' do + before do + allow(notification_client).to receive(:send_email) + end + + it 'logs to the Rails logger' do + # Necessary to allow multiple logging statements and test has_received on ours + # Required as other logging occurs (in lib/sidekiq/form526_job_status_tracker/job_tracker.rb callbacks) + allow(Rails.logger).to receive(:info) + exhaustion_time = Time.new(1985, 10, 26).utc + + Timecop.freeze(exhaustion_time) do + subject.perform_async(form526_submission.id, form_attachment.guid) + subject.drain + + expect(Rails.logger).to have_received(:info).with( + 'Form526DocumentUploadFailureEmail notification dispatched', + { + obscured_filename:, + form526_submission_id: form526_submission.id, + supporting_evidence_attachment_guid: form_attachment.guid, + timestamp: exhaustion_time + } + ) + end + end + + it 'increments a Statsd metric' do + # allow(notification_client).to receive(:send_email) + expect do + subject.perform_async(form526_submission.id, form_attachment.guid) + subject.drain + end.to trigger_statsd_increment( + 'api.form_526.veteran_notifications.document_upload_failure_email.success' + ) + end + + it 'creates a Form526JobStatus' do + expect do + subject.perform_async(form526_submission.id, form_attachment.guid) + subject.drain + end.to change(Form526JobStatus, :count).by(1) + end + end + end + + context 'when all retries are exhausted' do + let!(:form526_job_status) { create(:form526_job_status, :retryable_error, form526_submission:, job_id: 123) } + let(:retry_params) do + { + 'jid' => 123, + 'error_class' => 'JennyNotFound', + 'error_message' => 'I tried to call you before but I lost my nerve', + 'args' => [form526_submission.id, form_attachment.guid] + } + end + + let(:exhaustion_time) { DateTime.new(1985, 10, 26).utc } + + before do + allow(notification_client).to receive(:send_email) + end + + it 'increments a StatsD exhaustion metric, logs to the Rails logger and updates the job status' do + Timecop.freeze(exhaustion_time) do + described_class.within_sidekiq_retries_exhausted_block(retry_params) do + expect(Rails.logger).to receive(:warn).with( + 'Form526DocumentUploadFailureEmail retries exhausted', + { + job_id: 123, + error_class: 'JennyNotFound', + error_message: 'I tried to call you before but I lost my nerve', + timestamp: exhaustion_time, + form526_submission_id: form526_submission.id, + supporting_evidence_attachment_guid: form_attachment.guid + } + ).and_call_original + expect(StatsD).to receive(:increment).with( + 'api.form_526.veteran_notifications.document_upload_failure_email.exhausted' + ) + end + + form526_job_status.reload + expect(form526_job_status.status).to eq(Form526JobStatus::STATUS[:exhausted]) + end + end + end +end