diff --git a/app/services/evss_claim_service.rb b/app/services/evss_claim_service.rb index 0c390668bd..366ec24996 100644 --- a/app/services/evss_claim_service.rb +++ b/app/services/evss_claim_service.rb @@ -53,7 +53,7 @@ def request_decision(claim) # upload file to s3 and enqueue job to upload to EVSS, used by Claim Status Tool # EVSS::DocumentsService is where the uploading of documents actually happens def upload_document(evss_claim_document) - uploader = EVSSClaimDocumentUploader.new(@user.uuid, evss_claim_document.uploader_ids) + uploader = EVSSClaimDocumentUploader.new(@user.user_account_uuid, evss_claim_document.uploader_ids) uploader.store!(evss_claim_document.file_obj) # the uploader sanitizes the filename before storing, so set our doc to match @@ -63,7 +63,8 @@ def upload_document(evss_claim_document) headers = auth_headers.clone headers_supplemented = supplement_auth_headers(evss_claim_document.evss_claim_id, headers) - job_id = EVSS::DocumentUpload.perform_async(headers, @user.uuid, evss_claim_document.to_serializable_hash) + job_id = EVSS::DocumentUpload.perform_async(headers, @user.user_account_uuid, + evss_claim_document.to_serializable_hash) record_workaround('document_upload', evss_claim_document.evss_claim_id, job_id) if headers_supplemented diff --git a/app/sidekiq/evss/document_upload.rb b/app/sidekiq/evss/document_upload.rb index ca8475e3cb..36bc031130 100644 --- a/app/sidekiq/evss/document_upload.rb +++ b/app/sidekiq/evss/document_upload.rb @@ -8,6 +8,12 @@ class EVSS::DocumentUpload include Sidekiq::Job extend Logging::ThirdPartyTransaction::MethodWrapper + FILENAME_EXTENSION_MATCHER = /\.\w*$/ + OBFUSCATED_CHARACTER_MATCHER = /[a-zA-Z\d]/ + + NOTIFY_SETTINGS = Settings.vanotify.services.benefits_management_tools + MAILER_TEMPLATE_ID = NOTIFY_SETTINGS.template_id.evidence_submission_failure_email + attr_accessor :auth_headers, :user_uuid, :document_hash wrap_with_logging( @@ -29,6 +35,31 @@ class EVSS::DocumentUpload rand(3600..3660) if count < 9 end + sidekiq_retries_exhausted do |msg, _ex| + # There should be 3 args: + # 1) Auth headers needed to authenticate with EVSS + # 2) The uuid of the record in the UserAccount table + # 3) Document metadata + + next unless Flipper.enabled?('cst_send_evidence_failure_emails') + + icn = UserAccount.find(msg['args'][1]).icn + first_name = msg['args'].first['va_eauth_firstName'].titleize + filename = obscured_filename(msg['args'][2]['file_name']) + date_submitted = format_issue_instant_for_mailers(msg['created_at']) + + notify_client.send_email( + recipient_identifier: { id_value: icn, id_type: 'ICN' }, + template_id: MAILER_TEMPLATE_ID, + personalisation: { first_name:, filename:, date_submitted: } + ) + + ::Rails.logger.info('EVSS::DocumentUpload exhaustion handler email sent') + rescue => e + ::Rails.logger.error('EVSS::DocumentUpload exhaustion handler email error', + { message: e.message }) + end + def perform(auth_headers, user_uuid, document_hash) @auth_headers = auth_headers @user_uuid = user_uuid @@ -40,6 +71,33 @@ def perform(auth_headers, user_uuid, document_hash) clean_up! end + def self.obscured_filename(original_filename) + extension = original_filename[FILENAME_EXTENSION_MATCHER] + filename_without_extension = original_filename.gsub(FILENAME_EXTENSION_MATCHER, '') + + if filename_without_extension.length > 5 + # Obfuscate with the letter 'X'; we cannot obfuscate with special characters such as an asterisk, + # as these filenames appear in VA Notify Mailers and their templating engine uses markdown. + # Therefore, special characters can be interpreted as markdown and introduce formatting issues in the mailer + obfuscated_portion = filename_without_extension[3..-3].gsub(OBFUSCATED_CHARACTER_MATCHER, 'X') + filename_without_extension[0..2] + obfuscated_portion + filename_without_extension[-2..] + extension + else + original_filename + end + end + + def self.format_issue_instant_for_mailers(issue_instant) + # We want to return all times in EDT + timestamp = Time.at(issue_instant).in_time_zone('America/New_York') + + # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" + timestamp.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') + end + + def self.notify_client + VaNotify::Service.new(NOTIFY_SETTINGS.api_key) + end + private def validate_document! diff --git a/config/settings.yml b/config/settings.yml index 9f6f33c3fa..71a94e5b8f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1372,6 +1372,10 @@ vanotify: form526_document_upload_failure_notification_template_id: form526_document_upload_failure_notification_template_id form4142_upload_failure_notification_template_id: form4142_upload_failure_notification_template_id form0781_upload_failure_notification_template_id: form0781_upload_failure_notification_template_id + benefits_management_tools: + api_key: fake_secret + template_id: + evidence_submission_failure_email: fake_template_id ivc_champva: api_key: fake_secret template_id: diff --git a/config/settings/test.yml b/config/settings/test.yml index 6352a32d9f..956eff1f36 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -158,6 +158,10 @@ vanotify: claim_submission_duplicate_text: oh_fake_duplicate_template_id claim_submission_timeout_text: oh_fake_timeout_template_id claim_submission_error_text: oh_fake_error_template_id + benefits_management_tools: + api_key: fake_secret + template_id: + evidence_submission_failure_email: fake_template_id res: base_url: https://fake_url.com diff --git a/spec/sidekiq/evss/document_upload_spec.rb b/spec/sidekiq/evss/document_upload_spec.rb index 98732c3ae0..42f124ee7f 100644 --- a/spec/sidekiq/evss/document_upload_spec.rb +++ b/spec/sidekiq/evss/document_upload_spec.rb @@ -5,8 +5,14 @@ require 'evss/document_upload' RSpec.describe EVSS::DocumentUpload, type: :job do + subject { described_class } + let(:client_stub) { instance_double('EVSS::DocumentsService') } + let(:notify_client_stub) { instance_double('VaNotify::Sidekiq') } let(:uploader_stub) { instance_double('EVSSClaimDocumentUploader') } + + let(:user_account) { create(:user_account) } + let(:user_account_uuid) { user_account.id } let(:user) { FactoryBot.create(:user, :loa3) } let(:filename) { 'doctors-note.pdf' } let(:document_data) do @@ -19,6 +25,18 @@ end let(:auth_headers) { EVSS::AuthHeaders.new(user).to_h } + let(:issue_instant) { Time.now.to_i } + let(:args) do + { + 'args' => [{ 'va_eauth_firstName' => 'Bob' }, user_account_uuid, { 'file_name' => filename }], + 'created_at' => issue_instant + } + end + + before do + allow(Rails.logger).to receive(:info) + end + it 'retrieves the file and uploads to EVSS' do allow(EVSSClaimDocumentUploader).to receive(:new) { uploader_stub } allow(EVSS::DocumentsService).to receive(:new) { client_stub } @@ -29,4 +47,56 @@ expect(client_stub).to receive(:upload).with(file, document_data) described_class.new.perform(auth_headers, user.uuid, document_data.to_serializable_hash) end + + context 'when cst_send_evidence_failure_emails is enabled' do + before do + Flipper.enable(:cst_send_evidence_failure_emails) + end + + let(:formatted_submit_date) do + # We want to return all times in EDT + timestamp = Time.at(issue_instant).in_time_zone('America/New_York') + + # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" + timestamp.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') + end + + it 'enqueues a failure notification mailer to send to the veteran' do + allow(VaNotify::Service).to receive(:new) { notify_client_stub } + + subject.within_sidekiq_retries_exhausted_block(args) do + expect(notify_client_stub).to receive(:send_email).with( + { + recipient_identifier: { id_value: user_account.icn, id_type: 'ICN' }, + template_id: 'fake_template_id', + personalisation: { + first_name: 'Bob', + filename: 'docXXXX-XXte.pdf', + date_submitted: formatted_submit_date + } + } + ) + + expect(Rails.logger) + .to receive(:info) + .with('EVSS::DocumentUpload exhaustion handler email sent') + end + end + end + + context 'when cst_send_evidence_failure_emails is disabled' do + before do + Flipper.disable(:cst_send_evidence_failure_emails) + end + + let(:issue_instant) { Time.now.to_i } + + it 'does not enqueue a failure notification mailer to send to the veteran' do + allow(VaNotify::Service).to receive(:new) { notify_client_stub } + + subject.within_sidekiq_retries_exhausted_block(args) do + expect(notify_client_stub).not_to receive(:send_email) + end + end + end end