Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API-34448] VA Notify - Send declined notification #19362

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: ~
Comment on lines +677 to +680
Copy link
Contributor Author

@tycol7 tycol7 Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to update these values in prod/AWS?

Also, if testing locally, update these values (found in the ticket) and the VA Notify Lighthouse API key in local settings.

Copy link
Contributor

@rockwellwindsor-va rockwellwindsor-va Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and we will need to do a manifest update for the change in the name on those as well as the addition of the new ones.

Here is the previous PR for the accepted values if that helps at all. the Production manifest update will have to wait until all the templates get approved so you will only need to worry about the lower env additions/updates (prod & lower have to be separate PRs either way), but will want to create, or have a ticket created, for the prod ENV additions (there should be one in there already for that so same thing

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 @@ -33,28 +33,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 @@ -94,6 +93,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')
tycol7 marked this conversation as resolved.
Show resolved Hide resolved
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"'
)
tycol7 marked this conversation as resolved.
Show resolved Hide resolved
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:)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meaning we could also make this call in the job so we don't have to send first name and poa code to the decline job?

https://github.com/department-of-veterans-affairs/vets-api/pull/19362/files#r1842665735

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I think the fewer dependencies on BGS the better.


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
tycol7 marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per comment, participant id can be used to send emails.

If testing locally, change this line to email_address: '[email protected]',.

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_type_abbreviated: representative_type_text,
tycol7 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -30,6 +30,17 @@ def read_poa_request(poa_codes: [])
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 @@ -108,6 +108,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 @@ -260,9 +293,10 @@ def index_request_with(poa_codes:, auth_header:)
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
Loading