Skip to content

Commit

Permalink
[API-34448] VA Notify - Send declined notification (#19362)
Browse files Browse the repository at this point in the history
* rename VANotifyJob → VANotifyAcceptedJob

* add declined notify job

* add email content and templates

* add tests

* add poa code and participant id

* add test; fixup controller

* claimantFirstName → vetFirstName

* add claimantFirstName back

* remove VSOUserFirstName

* add representative id

* ptcpnt_id → participantId for user-facing

* procId → procID

* add base64 encoding for sidekiq params

* use slack_alert_on_failure

* use ordered arguments

* Update modules/claims_api/app/sidekiq/claims_api/va_notify_declined_job.rb

Co-authored-by: Rockwell Windsor Rice <129893414+rockwellwindsor-va@users.noreply.github.com>

* fall back to representative

---------

Co-authored-by: Rockwell Windsor Rice <129893414+rockwellwindsor-va@users.noreply.github.com>
  • Loading branch information
tycol7 and rockwellwindsor-va authored Nov 19, 2024
1 parent 6f56649 commit f4758c6
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 34 deletions.
6 changes: 4 additions & 2 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -674,8 +674,10 @@ claims_api:
poa_v2:
disable_jobs: false
vanotify:
representative_template_id: ~
service_organization_template_id: ~
accepted_representative_template_id: ~
accepted_service_organization_template_id: ~
declined_representative_template_id: ~
declined_service_organization_template_id: ~
services:
lighthouse:
api_key: ~
Expand Down
6 changes: 4 additions & 2 deletions config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,10 @@ claims_api:
aud_claim_url: https://fakeurlhere/fake/path/here
vanotify:
client_url: https://fakeurl/with/path/here
representative_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
service_organization_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
accepted_representative_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
accepted_service_organization_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
declined_representative_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
declined_service_organization_template_id: xxxxxx-zzzz-aaaa-bbbb-cccccccc
services:
lighthouse:
api_key: fake-xxxxxx-zzzz-aaaa-bbbb-cccccccc-xxxxxx-zzzz-aaaa-bbbb-cccccccc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,27 @@ def index

def decide
proc_id = form_attributes['procId']
ptcpnt_id = form_attributes['participantId']
decision = normalize(form_attributes['decision'])
representative_id = form_attributes['representativeId']

unless proc_id
raise ::Common::Exceptions::ParameterMissing.new('procId',
detail: 'procId is required')
end
validate_decide_params!(proc_id:, decision:)

decision = form_attributes['decision']
service = ManageRepresentativeService.new(external_uid: 'power_of_attorney_request_uid',
external_key: 'power_of_attorney_request_key')

unless decision && %w[accepted declined].include?(normalize(decision))
raise ::Common::Exceptions::ParameterMissing.new(
'decision',
detail: 'decision is required and must be either "ACCEPTED" or "DECLINED"'
)
if decision == 'declined'
poa_request = validate_ptcpnt_id!(ptcpnt_id:, proc_id:, representative_id:, service:)
end

service = ManageRepresentativeService.new(external_uid: 'power_of_attorney_request_uid',
external_key: 'power_of_attorney_request_key')
first_name = poa_request['claimantFirstName'] || poa_request['vetFirstName'].presence if poa_request

res = service.update_poa_request(proc_id:, secondary_status: decision,
declined_reason: form_attributes['declinedReason'])

raise ::Common::Exceptions::Lighthouse::BadGateway unless res
raise Common::Exceptions::Lighthouse::BadGateway if res.blank?

send_declined_notification(ptcpnt_id:, first_name:, representative_id:) if decision == 'declined'

render json: res, status: :ok
end
Expand Down Expand Up @@ -104,6 +103,53 @@ def create

private

def validate_decide_params!(proc_id:, decision:)
if proc_id.blank?
raise ::Common::Exceptions::ParameterMissing.new('procId',
detail: 'procId is required')
end

unless decision.present? && %w[accepted declined].include?(decision)
raise ::Common::Exceptions::ParameterMissing.new(
'decision',
detail: 'decision is required and must be either "ACCEPTED" or "DECLINED"'
)
end
end

def send_declined_notification(ptcpnt_id:, first_name:, representative_id:)
lockbox = Lockbox.new(key: Settings.lockbox.master_key)
encrypted_ptcpnt_id = Base64.strict_encode64(lockbox.encrypt(ptcpnt_id))
encrypted_first_name = Base64.strict_encode64(lockbox.encrypt(first_name))

ClaimsApi::VANotifyDeclinedJob.perform_async(encrypted_ptcpnt_id, encrypted_first_name, representative_id)
end

def validate_ptcpnt_id!(ptcpnt_id:, proc_id:, representative_id:, service:)
if ptcpnt_id.blank?
raise ::Common::Exceptions::ParameterMissing.new('ptcpntId',
detail: 'ptcpntId is required if decision is declined')
end

if representative_id.blank?
raise ::Common::Exceptions::ParameterMissing
.new('representativeId', detail: 'representativeId is required if decision is declined')
end

res = service.read_poa_request_by_ptcpnt_id(ptcpnt_id:)

raise ::Common::Exceptions::Lighthouse::BadGateway if res.blank?

poa_requests = Array.wrap(res['poaRequestRespondReturnVOList'])

matching_request = poa_requests.find { |poa_request| poa_request['procID'] == proc_id }

detail = 'Participant ID/Process ID combination not found'
raise ::Common::Exceptions::ResourceNotFound.new(detail:) if matching_request.nil?

matching_request
end

def validate_accredited_representative(registration_number, poa_code)
@representative = ::Veteran::Service::Representative.where('? = ANY(poa_codes) AND representative_id = ?',
poa_code,
Expand Down
2 changes: 1 addition & 1 deletion modules/claims_api/app/sidekiq/claims_api/poa_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def perform(power_of_attorney_id, rep = nil) # rubocop:disable Metrics/MethodLen

ClaimsApi::Logger.log('poa', poa_id: poa_form.id, detail: 'BIRLS Success')

ClaimsApi::VANotifyJob.perform_async(poa_form.id, rep) if vanotify?(poa_form.auth_headers, rep)
ClaimsApi::VANotifyAcceptedJob.perform_async(poa_form.id, rep) if vanotify?(poa_form.auth_headers, rep)

ClaimsApi::PoaVBMSUpdater.perform_async(poa_form.id) if enable_vbms_access?(poa_form:)
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# frozen_string_literal: true

module ClaimsApi
class VANotifyJob < ClaimsApi::ServiceBase
LOG_TAG = 'va_notify_job'
class VANotifyAcceptedJob < ClaimsApi::ServiceBase
LOG_TAG = 'va_notify_accepted_job'

def perform(poa_id, rep)
return if skip_notification_email?
Expand Down Expand Up @@ -39,7 +39,7 @@ def send_organization_notification(poa, org)
private

def handle_failure(poa_id, error)
job_name = 'ClaimsApi::VANotifyJob'
job_name = 'ClaimsApi::VANotifyAcceptedJob'
msg = "VA Notify email notification failed to send for #{poa_id} with error #{error}"
slack_alert_on_failure(job_name, msg)

Expand All @@ -64,7 +64,7 @@ def individual_accepted_email_contents(poa, rep)
email: value_or_default_for_field(rep.email),
phone: rep_phone(rep)
},
template_id: Settings.claims_api.vanotify.representative_template_id
template_id: Settings.claims_api.vanotify.accepted_representative_template_id
}
end

Expand All @@ -78,7 +78,7 @@ def organization_accepted_email_contents(poa, org)
location: value_or_default_for_field(org_location(org)),
phone: value_or_default_for_field(org.phone)
},
template_id: Settings.claims_api.vanotify.service_organization_template_id
template_id: Settings.claims_api.vanotify.accepted_service_organization_template_id
}
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

module ClaimsApi
class VANotifyDeclinedJob < ClaimsApi::ServiceBase
LOG_TAG = 'va_notify_declined_job'

def perform(encrypted_ptcpnt_id, encrypted_first_name, representative_id)
lockbox = Lockbox.new(key: Settings.lockbox.master_key)
ptcpnt_id = lockbox.decrypt(Base64.strict_decode64(encrypted_ptcpnt_id))
first_name = lockbox.decrypt(Base64.strict_decode64(encrypted_first_name))
representative = ::Veteran::Service::Representative.find_by(representative_id:)

if representative.blank?
raise ClaimsApi::Common::Exceptions::Lighthouse::ResourceNotFound.new(
detail: "Could not find veteran representative with id: #{representative_id}"
)
end

res = send_declined_notification(ptcpnt_id:, first_name:, representative:)

ClaimsApi::VANotifyFollowUpJob.perform_async(res.id) if res.present?
rescue => e
msg = "VA Notify email notification failed to send with error #{e}"
slack_alert_on_failure('ClaimsApi::VANotifyDeclinedJob', msg)

ClaimsApi::Logger.log(LOG_TAG, detail: msg)

raise e
end

private

def find_poa(poa_code:)
ClaimsApi::PowerOfAttorney.find do |poa|
poa.form_data.dig('serviceOrganization', 'poaCode') == poa_code ||
poa.form_data.dig('representative', 'poaCode') == poa_code
end
end

def send_declined_notification(ptcpnt_id:, first_name:, representative:)
representative_type = representative.user_type
return send_organization_notification(ptcpnt_id:, first_name:) if representative_type == 'veteran_service_officer'

send_representative_notification(ptcpnt_id:, first_name:, representative_type:)
end

def send_organization_notification(ptcpnt_id:, first_name:)
content = {
recipient_identifier: ptcpnt_id,
personalisation: {
first_name: first_name || '',
form_type: 'Appointment of Veterans Service Organization as Claimantʼs Representative (VA Form 21-22)'
},
template_id: Settings.claims_api.vanotify.declined_service_organization_template_id
}

vanotify_service.send_email(content)
end

def send_representative_notification(ptcpnt_id:, first_name:, representative_type:)
representative_type_text = get_representative_type_text(representative_type:)

content = {
recipient_identifier: ptcpnt_id,
personalisation: {
first_name: first_name || '',
representative_type: representative_type_text || 'representative',
representative_type_abbreviated: representative_type_text || 'representative',
form_type: 'Appointment of Individual as Claimantʼs Representative (VA Form 21-22a)'
},
template_id: Settings.claims_api.vanotify.declined_representative_template_id
}

vanotify_service.send_email(content)
end

def get_representative_type_text(representative_type:)
case representative_type
when 'attorney'
'attorney'
when 'claim_agents'
'claims agent'
end
end

def vanotify_service
VaNotify::Service.new(Settings.claims_api.vanotify.services.lighthouse.api_key)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ def read_poa_request(poa_codes: [], page_size: nil, page_index: nil, filter: {})
namespaces: { 'data' => '/data' }, transform_response: false)
end

def read_poa_request_by_ptcpnt_id(ptcpnt_id:)
builder = Nokogiri::XML::Builder.new do
PtcpntId ptcpnt_id
end

body = builder_to_xml(builder)

make_request(endpoint: bean_name, action: 'readPOARequestByPtcpntId', body:, key: 'POARequestRespondReturnVO',
namespaces: { 'data' => '/data' }, transform_response: false)
end

def update_poa_request(proc_id:, representative: {}, secondary_status: 'obsolete', declined_reason: nil)
first_name = representative[:first_name].presence || 'vets-api'
last_name = representative[:last_name].presence || 'vets-api'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,39 @@
end
end

context 'when the decision is declined and a ptcpntId is present' do
let(:service) { instance_double(ClaimsApi::ManageRepresentativeService) }
let(:poa_request_response) do
{
'poaRequestRespondReturnVOList' => [
{
'procID' => '76529',
'claimantFirstName' => 'John',
'poaCode' => '123'
}
]
}
end
let(:mock_lockbox) { double('Lockbox', encrypt: 'encrypted value') }

before do
allow(ClaimsApi::ManageRepresentativeService).to receive(:new).with(anything).and_return(service)
allow(service).to receive(:read_poa_request_by_ptcpnt_id).with(ptcpnt_id: '123456789')
.and_return(poa_request_response)
allow(service).to receive(:update_poa_request).with(anything).and_return('a successful response')
allow(Lockbox).to receive(:new).and_return(mock_lockbox)
end

it 'enqueues the VANotifyDeclinedJob' do
mock_ccg(scopes) do |auth_header|
expect do
decide_request_with(proc_id: '76529', decision: 'DECLINED', auth_header:, ptcpnt_id: '123456789',
representative_id: '456')
end.to change(ClaimsApi::VANotifyDeclinedJob.jobs, :size).by(1)
end
end
end

context 'when procId is present but invalid' do
let(:proc_id) { '1' }
let(:decision) { 'ACCEPTED' }
Expand Down Expand Up @@ -341,9 +374,10 @@ def index_request_with(poa_codes:, auth_header:, filter: {})
headers: auth_header
end

def decide_request_with(proc_id:, decision:, auth_header:)
def decide_request_with(proc_id:, decision:, auth_header:, ptcpnt_id: nil, representative_id: nil)
post v2_veterans_power_of_attorney_requests_decide_path,
params: { data: { attributes: { procId: proc_id, decision: } } }.to_json,
params: { data: { attributes: { procId: proc_id, decision:, participantId: ptcpnt_id,
representativeId: representative_id } } }.to_json,
headers: auth_header
end

Expand Down
8 changes: 4 additions & 4 deletions modules/claims_api/spec/sidekiq/poa_updater_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
})
poa.save!

expect(ClaimsApi::VANotifyJob).to receive(:perform_async)
expect(ClaimsApi::VANotifyAcceptedJob).to receive(:perform_async)

subject.new.perform(poa.id, 'Rep Data')
end
Expand All @@ -141,7 +141,7 @@
})
poa.save!

expect(ClaimsApi::VANotifyJob).not_to receive(:perform_async)
expect(ClaimsApi::VANotifyAcceptedJob).not_to receive(:perform_async)

subject.new.perform(poa.id, 'Rep Data')
end
Expand All @@ -154,13 +154,13 @@
})
poa.save!

expect(ClaimsApi::VANotifyJob).not_to receive(:perform_async)
expect(ClaimsApi::VANotifyAcceptedJob).not_to receive(:perform_async)

subject.new.perform(poa.id, nil)
end

it 'when the header key is not present' do
expect(ClaimsApi::VANotifyJob).not_to receive(:perform_async)
expect(ClaimsApi::VANotifyAcceptedJob).not_to receive(:perform_async)

subject.new.perform(poa.id, 'Rep data')
end
Expand Down
Loading

0 comments on commit f4758c6

Please sign in to comment.