From 62e01b6adc38fe1ff012fa71bb56237c94762b3b Mon Sep 17 00:00:00 2001 From: Holden Hinkle Date: Mon, 8 Apr 2024 11:10:18 -0400 Subject: [PATCH] Hh 77606 create poa endpoint (#16132) * create poa service and endpoint * wip * create #get_power_of_attorney in BenefitsClaims::Service * refactor PowerOfAttorneyController * delete RepresentationManagement::PowerOfAttorney::Service * create power_of_attorney route * add test for BenefitsClaims::Service #get_power_of_attorney * add new vcr cassette for power_of_attorney (must be updated) * write request specs for PowerOfAttorneyController * remove email from base serializer and add it to rep serializer * delete 'PowerOfAttorneyController', type: :request spec * create RepresentationManagement::V0::PowerOfAttorneyController, type: :controller spec instead * update endpoint response * formatting * add more tests, refactor * add serializer tests * add error handling to PowerOfAttorneyController #index * update icn used in testing poa endpoint * record vcr * remove settings * convert dig args to strings * order rep query by created_at DESC * correct the test name * fix failing tests * use #order in #find_representative * stub #order in test * remove private tests * refactor PowerOfAttorneyController * wip * add another test for rep poa response * refactor controller and add empty response vcr * add test for lighthouse responding with 422 * add request spec test for lighthouse responding with 422 * remove benefits claims service poa 422 response test --------- Co-authored-by: Jonathan VanCourt --- lib/lighthouse/benefits_claims/service.rb | 8 ++ .../v0/power_of_attorney_controller.rb | 65 +++++++++ .../power_of_attorney/base_serializer.rb | 21 +++ .../organization_serializer.rb | 14 ++ .../representative_serializer.rb | 23 +++ .../config/routes.rb | 1 + .../v0/power_of_attorney_request_spec.rb | 135 ++++++++++++++++++ .../power_of_attorney/base_serializer_spec.rb | 46 ++++++ .../organization_serializer_spec.rb | 35 +++++ .../representative_serializer_spec.rb | 36 +++++ .../benefits_claims/service_spec.rb | 21 +++ .../power_of_attorney/200_empty_response.yml | 77 ++++++++++ .../power_of_attorney/200_response.yml | 78 ++++++++++ 13 files changed, 560 insertions(+) create mode 100644 modules/representation_management/app/controllers/representation_management/v0/power_of_attorney_controller.rb create mode 100644 modules/representation_management/app/serializers/representation_management/power_of_attorney/base_serializer.rb create mode 100644 modules/representation_management/app/serializers/representation_management/power_of_attorney/organization_serializer.rb create mode 100644 modules/representation_management/app/serializers/representation_management/power_of_attorney/representative_serializer.rb create mode 100644 modules/representation_management/spec/requests/v0/power_of_attorney_request_spec.rb create mode 100644 modules/representation_management/spec/serializers/power_of_attorney/base_serializer_spec.rb create mode 100644 modules/representation_management/spec/serializers/power_of_attorney/organization_serializer_spec.rb create mode 100644 modules/representation_management/spec/serializers/power_of_attorney/representative_serializer_spec.rb create mode 100644 spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_empty_response.yml create mode 100644 spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_response.yml diff --git a/lib/lighthouse/benefits_claims/service.rb b/lib/lighthouse/benefits_claims/service.rb index 3e4313cf0e1..46f200a43ea 100644 --- a/lib/lighthouse/benefits_claims/service.rb +++ b/lib/lighthouse/benefits_claims/service.rb @@ -38,6 +38,14 @@ def get_claim(id, lighthouse_client_id = nil, lighthouse_rsa_key_path = nil, opt raise BenefitsClaims::ServiceException.new(e.response), 'Lighthouse Error' end + def get_power_of_attorney(lighthouse_client_id = nil, lighthouse_rsa_key_path = nil, options = {}) + config.get("#{@icn}/power-of-attorney", lighthouse_client_id, lighthouse_rsa_key_path, options).body + rescue Faraday::TimeoutError + raise BenefitsClaims::ServiceException.new({ status: 504 }), 'Lighthouse Error' + rescue Faraday::ClientError, Faraday::ServerError => e + raise BenefitsClaims::ServiceException.new(e.response), 'Lighthouse Error' + end + def submit5103(user, id, options = {}) params = {} is_dependent = SponsorResolver.dependent?(user) diff --git a/modules/representation_management/app/controllers/representation_management/v0/power_of_attorney_controller.rb b/modules/representation_management/app/controllers/representation_management/v0/power_of_attorney_controller.rb new file mode 100644 index 00000000000..3ec5cfd7986 --- /dev/null +++ b/modules/representation_management/app/controllers/representation_management/v0/power_of_attorney_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'lighthouse/benefits_claims/service' + +module RepresentationManagement + module V0 + class PowerOfAttorneyController < ApplicationController + service_tag 'representation-management' + + def index + @active_poa = lighthouse_service.get_power_of_attorney + + if @active_poa.blank? || record.blank? + render json: { data: {} }, status: :ok + else + render json: record, serializer:, status: :ok + end + end + + private + + def lighthouse_service + BenefitsClaims::Service.new(icn) + end + + def icn + @current_user&.icn + end + + def poa_code + @poa_code ||= @active_poa.dig('data', 'attributes', 'code') + end + + def poa_type + @poa_type ||= @active_poa.dig('data', 'type') + end + + def record + return @record if defined? @record + + @record ||= if poa_type == 'organization' + organization + else + representative + end + end + + def serializer + if poa_type == 'organization' + RepresentationManagement::PowerOfAttorney::OrganizationSerializer + else + RepresentationManagement::PowerOfAttorney::RepresentativeSerializer + end + end + + def organization + Veteran::Service::Organization.find_by(poa: poa_code) + end + + def representative + Veteran::Service::Representative.where('? = ANY(poa_codes)', poa_code).order(created_at: :desc).first + end + end + end +end diff --git a/modules/representation_management/app/serializers/representation_management/power_of_attorney/base_serializer.rb b/modules/representation_management/app/serializers/representation_management/power_of_attorney/base_serializer.rb new file mode 100644 index 00000000000..1b35a8a0605 --- /dev/null +++ b/modules/representation_management/app/serializers/representation_management/power_of_attorney/base_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module RepresentationManagement + module PowerOfAttorney + class BaseSerializer < ActiveModel::Serializer + attribute :address_line1 + attribute :address_line2 + attribute :address_line3 + attribute :address_type + attribute :city + attribute :country_name + attribute :country_code_iso3 + attribute :province + attribute :international_postal_code + attribute :state_code + attribute :zip_code + attribute :zip_suffix + attribute :phone + end + end +end diff --git a/modules/representation_management/app/serializers/representation_management/power_of_attorney/organization_serializer.rb b/modules/representation_management/app/serializers/representation_management/power_of_attorney/organization_serializer.rb new file mode 100644 index 00000000000..c7f97d5b1f4 --- /dev/null +++ b/modules/representation_management/app/serializers/representation_management/power_of_attorney/organization_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module RepresentationManagement + module PowerOfAttorney + class OrganizationSerializer < BaseSerializer + attribute :type + attribute :name + + def type + 'organization' + end + end + end +end diff --git a/modules/representation_management/app/serializers/representation_management/power_of_attorney/representative_serializer.rb b/modules/representation_management/app/serializers/representation_management/power_of_attorney/representative_serializer.rb new file mode 100644 index 00000000000..cade1119fed --- /dev/null +++ b/modules/representation_management/app/serializers/representation_management/power_of_attorney/representative_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RepresentationManagement + module PowerOfAttorney + class RepresentativeSerializer < BaseSerializer + attribute :type + attribute :name + attribute :email + + def type + 'representative' + end + + def name + object.full_name + end + + def phone + object.phone_number + end + end + end +end diff --git a/modules/representation_management/config/routes.rb b/modules/representation_management/config/routes.rb index b5d3ba5ee0f..241b887ca2e 100644 --- a/modules/representation_management/config/routes.rb +++ b/modules/representation_management/config/routes.rb @@ -3,5 +3,6 @@ RepresentationManagement::Engine.routes.draw do namespace :v0, defaults: { format: 'json' } do resources :flag_accredited_representatives, only: %i[create] + resources :power_of_attorney, only: %i[index] end end diff --git a/modules/representation_management/spec/requests/v0/power_of_attorney_request_spec.rb b/modules/representation_management/spec/requests/v0/power_of_attorney_request_spec.rb new file mode 100644 index 00000000000..80dd5490738 --- /dev/null +++ b/modules/representation_management/spec/requests/v0/power_of_attorney_request_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'RepresentationManagement::V0::PowerOfAttorneyController', type: :request do + let(:index_path) { '/representation_management/v0/power_of_attorney' } + let(:user) { create(:user, :loa3) } + + describe 'index' do + context 'with a signed in user' do + before do + sign_in_as(user) + end + + context 'when an organization is the active poa' do + let(:org_poa) { 'og1' } + let!(:organization) { create(:organization, poa: org_poa) } + + it 'returns the expected organization response' do + lh_response = { + 'data' => { + 'type' => 'organization', + 'attributes' => { + 'code' => org_poa + } + } + } + allow_any_instance_of(BenefitsClaims::Service) + .to receive(:get_power_of_attorney) + .and_return(lh_response) + + get index_path + + response_body = JSON.parse(response.body) + + expect(response).to have_http_status(:ok) + expect(response_body['data']['id']).to eq(org_poa) + end + end + + context 'when a representative is the active poa' do + let(:rep_poa) { 'rp1' } + let(:registration_number) { '12345' } + let!(:representative) do + create(:representative, + representative_id: registration_number, poa_codes: [rep_poa]) + end + + it 'returns the expected representative response' do + lh_response = { + 'data' => { + 'type' => 'individual', + 'attributes' => { + 'code' => rep_poa + } + } + } + allow_any_instance_of(BenefitsClaims::Service) + .to receive(:get_power_of_attorney) + .and_return(lh_response) + + get index_path + + response_body = JSON.parse(response.body) + + expect(response).to have_http_status(:ok) + expect(response_body['data']['id']).to eq(registration_number) + end + end + + context 'when there is no active poa' do + it 'returns the expected empty response' do + lh_response = { + 'data' => {} + } + allow_any_instance_of(BenefitsClaims::Service) + .to receive(:get_power_of_attorney) + .and_return(lh_response) + + get index_path + + response_body = JSON.parse(response.body) + + expect(response).to have_http_status(:ok) + expect(response_body['data']).to eq({}) + end + end + + context 'when the poa record is not found in the database' do + it 'returns the expected empty response' do + lh_response = { + 'data' => { + 'type' => 'organization', + 'attributes' => { + 'code' => 'abc' + } + } + } + allow_any_instance_of(BenefitsClaims::Service) + .to receive(:get_power_of_attorney) + .and_return(lh_response) + + get index_path + + response_body = JSON.parse(response.body) + + expect(response).to have_http_status(:ok) + expect(response_body['data']).to eq({}) + end + end + + context 'when the service encounters an unprocessable entity error' do + it 'returns a 422/unprocessable_entity status' do + allow_any_instance_of(BenefitsClaims::Service) + .to receive(:get_power_of_attorney) + .and_raise(Common::Exceptions::UnprocessableEntity) + + get index_path + + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + context 'without a signed in user' do + describe 'GET #index' do + it 'returns a 401/unauthorized status' do + get index_path + + expect(response).to have_http_status(:unauthorized) + end + end + end + end +end diff --git a/modules/representation_management/spec/serializers/power_of_attorney/base_serializer_spec.rb b/modules/representation_management/spec/serializers/power_of_attorney/base_serializer_spec.rb new file mode 100644 index 00000000000..1daa886c08c --- /dev/null +++ b/modules/representation_management/spec/serializers/power_of_attorney/base_serializer_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'BaseSerializer' do + before do + create(:organization, poa: 'og1') + create(:representative, representative_id: '123', poa_codes: ['rp1']) + end + + def representative + Veteran::Service::Representative.find('123') + end + + def organization + Veteran::Service::Organization.find('og1') + end + + def assert_attributes(attributes) + expect(attributes.keys).to eq(%w[address_line1 + address_line2 + address_line3 + address_type + city + country_name + country_code_iso3 + province + international_postal_code + state_code + zip_code + zip_suffix + phone]) + end + + it 'can serialize a representative' do + result = serialize(representative, serializer_class: RepresentationManagement::PowerOfAttorney::BaseSerializer) + attributes = JSON.parse(result)['data']['attributes'] + assert_attributes(attributes) + end + + it 'can serialize an organization' do + result = serialize(organization, serializer_class: RepresentationManagement::PowerOfAttorney::BaseSerializer) + attributes = JSON.parse(result)['data']['attributes'] + assert_attributes(attributes) + end +end diff --git a/modules/representation_management/spec/serializers/power_of_attorney/organization_serializer_spec.rb b/modules/representation_management/spec/serializers/power_of_attorney/organization_serializer_spec.rb new file mode 100644 index 00000000000..9ec39fbe579 --- /dev/null +++ b/modules/representation_management/spec/serializers/power_of_attorney/organization_serializer_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'OrganizationSerializer' do + before do + create(:organization, poa: 'og1') + end + + def organization + Veteran::Service::Organization.find('og1') + end + + it 'can serialize an organization' do + result = serialize(organization, + serializer_class: RepresentationManagement::PowerOfAttorney::OrganizationSerializer) + attributes = JSON.parse(result)['data']['attributes'] + + expect(attributes.keys).to eq(%w[address_line1 + address_line2 + address_line3 + address_type + city + country_name + country_code_iso3 + province + international_postal_code + state_code + zip_code + zip_suffix + phone + type + name]) + end +end diff --git a/modules/representation_management/spec/serializers/power_of_attorney/representative_serializer_spec.rb b/modules/representation_management/spec/serializers/power_of_attorney/representative_serializer_spec.rb new file mode 100644 index 00000000000..c39ea43ec7e --- /dev/null +++ b/modules/representation_management/spec/serializers/power_of_attorney/representative_serializer_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'RepresentativeSerializer' do + before do + create(:representative, representative_id: '123', poa_codes: ['rp1']) + end + + def representative + Veteran::Service::Representative.find('123') + end + + it 'can serialize a representative' do + result = serialize(representative, + serializer_class: RepresentationManagement::PowerOfAttorney::RepresentativeSerializer) + attributes = JSON.parse(result)['data']['attributes'] + + expect(attributes.keys).to eq(%w[address_line1 + address_line2 + address_line3 + address_type + city + country_name + country_code_iso3 + province + international_postal_code + state_code + zip_code + zip_suffix + phone + type + name + email]) + end +end diff --git a/spec/lib/lighthouse/benefits_claims/service_spec.rb b/spec/lib/lighthouse/benefits_claims/service_spec.rb index 27035f97ea0..d5ecd4fe022 100644 --- a/spec/lib/lighthouse/benefits_claims/service_spec.rb +++ b/spec/lib/lighthouse/benefits_claims/service_spec.rb @@ -56,6 +56,27 @@ end end + describe "when requesting a user's power of attorney" do + context 'when the user has an active power of attorney' do + it 'retrieves the power of attorney from the Lighthouse API' do + VCR.use_cassette('lighthouse/benefits_claims/power_of_attorney/200_response') do + response = @service.get_power_of_attorney + expect(response['data']['type']).to eq('individual') + expect(response['data']['attributes']['code']).to eq('067') + end + end + end + + context 'when the user does not have an active power of attorney' do + it 'retrieves the power of attorney from the Lighthouse API' do + VCR.use_cassette('lighthouse/benefits_claims/power_of_attorney/200_empty_response') do + response = @service.get_power_of_attorney + expect(response['data']).to eq({}) + end + end + end + end + describe 'when posting a form526' do it 'when given a full request body, posts to the Lighthouse API' do VCR.use_cassette('lighthouse/benefits_claims/submit526/200_response') do diff --git a/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_empty_response.yml b/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_empty_response.yml new file mode 100644 index 00000000000..d7878dd339e --- /dev/null +++ b/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_empty_response.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: get + uri: https://sandbox-api.va.gov/services/claims/v2/veterans/123498767V234859/power-of-attorney + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Vets.gov Agent + Authorization: Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 04 Apr 2024 17:45:59 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '11' + Connection: + - keep-alive + Ratelimit-Remaining: + - '118' + Ratelimit-Reset: + - '3' + X-Ratelimit-Limit-Minute: + - '120' + X-Ratelimit-Remaining-Minute: + - '118' + Ratelimit-Limit: + - '120' + Etag: + - W/"7fb9d166d1a15bce0b9f085f3818946f" + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Git-Sha: + - 77911592048236d8eccb0f39f5e9f758564e282b + X-Github-Repository: + - https://github.com/department-of-veterans-affairs/vets-api + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 8dd9e3bb-6159-4f92-b92d-7dbe791547e0 + X-Runtime: + - '2.017096' + X-Xss-Protection: + - '0' + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cache-Control: + - no-cache, no-store + Pragma: + - no-cache + body: + encoding: UTF-8 + string: '{"data":{}}' + recorded_at: Thu, 04 Apr 2024 17:46:00 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_response.yml b/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_response.yml new file mode 100644 index 00000000000..bcf3bbe002d --- /dev/null +++ b/spec/support/vcr_cassettes/lighthouse/benefits_claims/power_of_attorney/200_response.yml @@ -0,0 +1,78 @@ +--- +http_interactions: +- request: + method: get + uri: https://sandbox-api.va.gov/services/claims/v2/veterans/123498767V234859/power-of-attorney + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Vets.gov Agent + Authorization: Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 02 Apr 2024 14:30:58 GMT + Content-Type: + - application/json; charset=utf-8 + Connection: + - keep-alive + X-Ratelimit-Remaining-Minute: + - '119' + X-Ratelimit-Limit-Minute: + - '120' + Ratelimit-Remaining: + - '119' + Ratelimit-Limit: + - '120' + Ratelimit-Reset: + - '10' + Etag: + - W/"f674f2056c77718a448e94dc8a1ed556" + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Git-Sha: + - 11de6c4d9dc0e22795b79e94395598e9c7e0fc3c + X-Github-Repository: + - https://github.com/department-of-veterans-affairs/vets-api + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - f7395f08-c7c7-4726-85e4-602c6ecf40fe + X-Runtime: + - '8.308072' + X-Xss-Protection: + - '0' + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cache-Control: + - no-cache, no-store + Pragma: + - no-cache + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: '{"data":{"type":"individual","attributes":{"code":"067","name":"Tamara + Ellis","phoneNumber":null}}}' + recorded_at: Tue, 02 Apr 2024 14:30:58 GMT +recorded_with: VCR 6.2.0