diff --git a/modules/claims_api/app/controllers/claims_api/v2/blueprints/power_of_attorney_request_blueprint.rb b/modules/claims_api/app/controllers/claims_api/v2/blueprints/power_of_attorney_request_blueprint.rb new file mode 100644 index 00000000000..5caa1767e3e --- /dev/null +++ b/modules/claims_api/app/controllers/claims_api/v2/blueprints/power_of_attorney_request_blueprint.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module ClaimsApi + module V2 + module Blueprints + class PowerOfAttorneyRequestBlueprint < Blueprinter::Base + class Veteran < Blueprinter::Base + transform Transformers::LowerCamelTransformer + + fields( + :first_name, + :middle_name, + :last_name, + :participant_id + ) + end + + class Representative < Blueprinter::Base + transform Transformers::LowerCamelTransformer + + fields( + :first_name, + :last_name, + :email + ) + end + + class Claimant < Blueprinter::Base + transform Transformers::LowerCamelTransformer + + fields( + :first_name, + :last_name, + :participant_id, + :relationship_to_veteran + ) + end + + class Address < Blueprinter::Base + transform Transformers::LowerCamelTransformer + + fields( + :city, :state, :zip, :country, + :military_post_office, + :military_postal_code + ) + end + + class Attributes < Blueprinter::Base + transform Transformers::LowerCamelTransformer + + fields( + :status, + :declined_reason, + :power_of_attorney_code + ) + + field( + :submitted_at, + datetime_format: :iso8601.to_proc + ) + + field( + :accepted_or_declined_at, + datetime_format: :iso8601.to_proc + ) + + field( + :authorizes_address_changing?, + name: :is_address_changing_authorized + ) + + field( + :authorizes_treatment_disclosure?, + name: :is_treatment_disclosure_authorized + ) + + association :veteran, blueprint: Veteran + association :representative, blueprint: Representative + association :claimant, blueprint: Claimant + association :claimant_address, blueprint: Address + end + + transform Transformers::LowerCamelTransformer + + identifier :id + field(:type) { 'powerOfAttorneyRequest' } + + association :attributes, blueprint: Attributes do |poa_request| + poa_request + end + end + end + end +end diff --git a/modules/claims_api/app/controllers/claims_api/v2/power_of_attorney_requests_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/power_of_attorney_requests_controller.rb new file mode 100644 index 00000000000..bfeda218c62 --- /dev/null +++ b/modules/claims_api/app/controllers/claims_api/v2/power_of_attorney_requests_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'bgs_service/manage_representative_service' + +module ClaimsApi + module V2 + class PowerOfAttorneyRequestsController < ClaimsApi::V2::ApplicationController + def index + poa_requests = ClaimsApi::PowerOfAttorneyRequestService::Search.perform + render json: Blueprints::PowerOfAttorneyRequestBlueprint.render(poa_requests, root: :data) + end + end + end +end diff --git a/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/poa_request.rb b/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/poa_request.rb new file mode 100644 index 00000000000..3869c7f6ee7 --- /dev/null +++ b/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/poa_request.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module ClaimsApi + module PowerOfAttorneyRequestService + # Notice we're not bothering to memoize many of the instance methods which + # are very small calculations. + # + # TODO: Document philosophy around validity of source data? + class PoaRequest + module Statuses + ALL = [ + NEW = 'New', + PENDING = 'Pending', + ACCEPTED = 'Accepted', + DECLINED = 'Declined' + ].freeze + end + + Veteran = + Data.define( + :first_name, + :middle_name, + :last_name, + :participant_id + ) + + Representative = + Data.define( + :first_name, + :last_name, + :email + ) + + Claimant = + Data.define( + :first_name, + :last_name, + :participant_id, + :relationship_to_veteran + ) + + Address = + Data.define( + :city, :state, :zip, :country, + :military_post_office, + :military_postal_code + ) + + def initialize(data) + @data = data + end + + def id + @data['procID'].to_i + end + + def status + @data['secondaryStatus'].presence_in(Statuses::ALL) + end + + def submitted_at + Utilities.time(@data['dateRequestReceived']) + end + + def accepted_or_declined_at + Utilities.time(@data['dateRequestActioned']) + end + + def declined_reason + if status == Statuses::DECLINED # rubocop:disable Style/IfUnlessModifier + @data['declinedReason'] + end + end + + def authorizes_address_changing? + Utilities.boolean(@data['changeAddressAuth']) + end + + def authorizes_treatment_disclosure? + Utilities.boolean(@data['healthInfoAuth']) + end + + def power_of_attorney_code + @data['poaCode'] + end + + def veteran + @veteran ||= + Veteran.new( + first_name: @data['vetFirstName'], + middle_name: @data['vetMiddleName'], + last_name: @data['vetLastName'], + # TODO: Gotta figure out if this is always present or not. + participant_id: @data['vetPtcpntID']&.to_i + ) + end + + def representative + @representative ||= + Representative.new( + first_name: @data['VSOUserFirstName'], + last_name: @data['VSOUserLastName'], + email: @data['VSOUserEmail'] + ) + end + + def claimant + @claimant ||= begin + # TODO: Check on `claimantRelationship` values in BGS. + relationship = @data['claimantRelationship'] + if relationship.present? && relationship != 'Self' + Claimant.new( + first_name: @data['claimantFirstName'], + last_name: @data['claimantLastName'], + participant_id: @data['claimantPtcpntID'].to_i, + relationship_to_veteran: relationship + ) + end + end + end + + def claimant_address + @claimant_address ||= + Address.new( + city: @data['claimantCity'], + state: @data['claimantState'], + zip: @data['claimantZip'], + country: @data['claimantCountry'], + military_post_office: @data['claimantMilitaryPO'], + military_postal_code: @data['claimantMilitaryPostalCode'] + ) + end + + module Utilities + class << self + def time(value) + ActiveSupport::TimeZone['UTC'].parse(value.to_s) + end + + def boolean(value) + case value + when 'Y' + true + when 'N' + false + else # rubocop:disable Style/EmptyElse + # Just to be explicit. + nil + end + end + end + end + end + end +end diff --git a/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/search.rb b/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/search.rb new file mode 100644 index 00000000000..58b79d6fd9a --- /dev/null +++ b/modules/claims_api/app/services/claims_api/power_of_attorney_request_service/search.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ClaimsApi + module PowerOfAttorneyRequestService + module Search + class << self + def perform + # `Array.wrap` (the `ActiveSupport` core extension with nicer behavior + # than Ruby core) because upstream invocation of `Hash.from_xml` has + # different output depending on the cardinality of sibling XML + # elements for a given kind: + # 0 => Absent + # 1 => Object + # >1 => Array + poa_requests = make_request['poaRequestRespondReturnVOList'] + Array.wrap(poa_requests).map { |data| PoaRequest.new(data) } + end + + private + + def make_request + bgs_client = + ClaimsApi::ManageRepresentativeService.new( + external_uid: 'xUid', + external_key: 'xKey' + ) + + bgs_client.read_poa_request( + poa_codes: ['012'], + statuses: %w[ + new + pending + accepted + declined + ] + ) + end + end + end + end +end diff --git a/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json b/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json index 9664e172d84..7094044200c 100644 --- a/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json +++ b/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json @@ -69,6 +69,289 @@ } }, "paths": { + "/power-of-attorney-requests": { + "get": { + "summary": "Search for Power of Attorney requests.", + "tags": [ + "Power of Attorney" + ], + "operationId": "searchPowerOfAttorneyRequests", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "description": "Faceted, paginated, and sorted search of Power of Attorney requests", + "responses": { + "200": { + "description": "Search results", + "content": { + "application/json": { + "example": { + "data": [ + { + "id": 12345, + "type": "powerOfAttorneyRequest", + "attributes": { + "status": "Declined", + "declinedReason": "Because I felt like it", + "powerOfAttorneyCode": "012", + "submittedAt": "2024-04-10T04:51:12Z", + "acceptedOrDeclinedAt": "2024-04-10T04:51:12Z", + "isAddressChangingAuthorized": true, + "isTreatmentDisclosureAuthorized": false, + "veteran": { + "firstName": "Firstus", + "middleName": null, + "lastName": "Lastus", + "participantId": 600043200 + }, + "representative": { + "firstName": "Primero", + "lastName": "Ultimo", + "email": "primero.ultimo@vsorg.org" + }, + "claimant": { + "firstName": "Alpha", + "lastName": "Omega", + "participantId": 23456, + "relationshipToVeteran": "Cousin" + }, + "claimantAddress": { + "city": "Baltimore", + "state": "MD", + "zip": "21218", + "country": "US", + "militaryPostOffice": null, + "militaryPostalCode": null + } + } + } + ] + }, + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "description": "List of Power of Attorney requests satisfying the given search", + "items": { + "additionalProperties": false, + "required": [ + "type", + "id", + "attributes" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "powerOfAttorneyRequest" + ] + }, + "id": { + "type": "integer", + "description": "The ID of the form application process that uniquely identifies this Power of Attorney request", + "format": "int64" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "submittedAt", + "acceptedOrDeclinedAt", + "declinedReason", + "isAddressChangingAuthorized", + "isTreatmentDisclosureAuthorized", + "powerOfAttorneyCode", + "veteran", + "representative", + "claimant", + "claimantAddress" + ], + "properties": { + "status": { + "type": "string", + "description": "The Power of Attorney request's current status", + "enum": [ + "New", + "Pending", + "Accepted", + "Declined" + ] + }, + "submittedAt": { + "type": "string", + "description": "UTC datetime at which the Power of Attorney request was submitted", + "format": "date-time" + }, + "acceptedOrDeclinedAt": { + "type": "string", + "description": "UTC datetime at which the Power of Attorney request was accepted or declined", + "nullable": true, + "format": "date-time" + }, + "declinedReason": { + "type": "string", + "description": "The reason given by the representative for declining the Power of Attorney request", + "nullable": true + }, + "isAddressChangingAuthorized": { + "type": "boolean", + "description": "Whether the representative is authorized to change the claimant's address" + }, + "isTreatmentDisclosureAuthorized": { + "type": "boolean", + "description": "Whether the representative is authorized to receive disclosures of the Veteran's treatment records" + }, + "powerOfAttorneyCode": { + "type": "string", + "description": "The code that indicates an individual or organization's ability to be granted Power of Attorney for a Veteran" + }, + "veteran": { + "type": "object", + "description": "The Veteran for whom Power of Attorney is being requested", + "additionalProperties": false, + "required": [ + "firstName", + "middleName", + "lastName", + "participantId" + ], + "properties": { + "firstName": { + "type": "string" + }, + "middleName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string" + }, + "participantId": { + "type": "integer", + "description": "The identifier of the Veteran as executor of this form application process", + "format": "int64", + "nullable": true + } + } + }, + "representative": { + "type": "object", + "description": "The representative to whom this Power of Attorney request is being submitted", + "additionalProperties": false, + "required": [ + "firstName", + "lastName", + "email" + ], + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "claimant": { + "type": "object", + "description": "If applicable, the individual that executed this form application process for the Veteran", + "additionalProperties": false, + "nullable": true, + "required": [ + "firstName", + "lastName", + "participantId", + "relationshipToVeteran" + ], + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "participantId": { + "type": "integer", + "description": "The identifier of the individual as executor of this form application process", + "format": "int64" + }, + "relationshipToVeteran": { + "type": "string" + } + } + }, + "claimantAddress": { + "type": "object", + "description": "The mailing address of the individual as executor of this form application process", + "additionalProperties": false, + "required": [ + "city", + "state", + "zip", + "country", + "militaryPostOffice", + "militaryPostalCode" + ], + "properties": { + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "country": { + "type": "string" + }, + "militaryPostOffice": { + "type": "string", + "nullable": true + }, + "militaryPostalCode": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, "/veteran-id:find": { "post": { "summary": "Retrieve Veteran ID.", diff --git a/modules/claims_api/config/routes.rb b/modules/claims_api/config/routes.rb index a8fa7629e02..6f96c6ad271 100644 --- a/modules/claims_api/config/routes.rb +++ b/modules/claims_api/config/routes.rb @@ -62,6 +62,8 @@ post '/:veteranId/526/:id/attachments', to: 'disability_compensation#attachments' post '/:veteranId/526/generatePDF/minimum-validations', to: 'disability_compensation#generate_pdf' end + + resources :power_of_attorney_requests, path: 'power-of-attorney-requests', only: [:index] end namespace :docs do diff --git a/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb b/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb new file mode 100644 index 00000000000..b4125980b5e --- /dev/null +++ b/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'swagger_helper' +require Rails.root.join('spec', 'rswag_override.rb').to_s +require 'rails_helper' +require Rails.root.join('modules', 'claims_api', 'spec', 'rails_helper.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') + +metadata = { + openapi_spec: Rswag::TextHelpers.new.claims_api_docs, + production: false, + bgs: { + service: 'manage_representative_service', + operation: 'read_poa_request' + } +} + +describe 'PowerOfAttorney', metadata do + path '/power-of-attorney-requests' do + get 'Search for Power of Attorney requests.' do + tags 'Power of Attorney' + operationId 'searchPowerOfAttorneyRequests' + security [ + { productionOauth: ['system/claim.read', 'system/system/claim.write'] }, + { sandboxOauth: ['system/claim.read', 'system/system/claim.write'] }, + { bearer_token: [] } + ] + produces 'application/json' + description 'Faceted, paginated, and sorted search of Power of Attorney requests' + + let(:Authorization) { 'Bearer token' } + let(:scopes) { %w[system/claim.read system/system/claim.write] } + + response '200', 'Search results' do + schema JSON.parse(Rails.root.join( + 'spec', 'support', 'schemas', + 'claims_api', 'v2', 'power_of_attorney_requests', + 'index.json' + ).read) + + before do |example| + mock_ccg(scopes) do + use_bgs_cassette('nonempty') do + submit_request(example.metadata) + end + end + end + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + + it do |example| + assert_response_matches_metadata(example.metadata) + end + end + end + end +end diff --git a/spec/support/schemas/claims_api/v2/power_of_attorney_requests/index.json b/spec/support/schemas/claims_api/v2/power_of_attorney_requests/index.json new file mode 100644 index 00000000000..5efd7e5938a --- /dev/null +++ b/spec/support/schemas/claims_api/v2/power_of_attorney_requests/index.json @@ -0,0 +1,203 @@ +{ + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "description": "List of Power of Attorney requests satisfying the given search", + "items": { + "additionalProperties": false, + "required": [ + "type", + "id", + "attributes" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "powerOfAttorneyRequest" + ] + }, + "id": { + "type": "integer", + "description": "The ID of the form application process that uniquely identifies this Power of Attorney request", + "format": "int64" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "submittedAt", + "acceptedOrDeclinedAt", + "declinedReason", + "isAddressChangingAuthorized", + "isTreatmentDisclosureAuthorized", + "powerOfAttorneyCode", + "veteran", + "representative", + "claimant", + "claimantAddress" + ], + "properties": { + "status": { + "type": "string", + "description": "The Power of Attorney request's current status", + "enum": [ + "New", + "Pending", + "Accepted", + "Declined" + ] + }, + "submittedAt": { + "type": "string", + "description": "UTC datetime at which the Power of Attorney request was submitted", + "format": "date-time" + }, + "acceptedOrDeclinedAt": { + "type": "string", + "description": "UTC datetime at which the Power of Attorney request was accepted or declined", + "nullable": true, + "format": "date-time" + }, + "declinedReason": { + "type": "string", + "description": "The reason given by the representative for declining the Power of Attorney request", + "nullable": true + }, + "isAddressChangingAuthorized": { + "type": "boolean", + "description": "Whether the representative is authorized to change the claimant's address" + }, + "isTreatmentDisclosureAuthorized": { + "type": "boolean", + "description": "Whether the representative is authorized to receive disclosures of the Veteran's treatment records" + }, + "powerOfAttorneyCode": { + "type": "string", + "description": "The code that indicates an individual or organization's ability to be granted Power of Attorney for a Veteran" + }, + "veteran": { + "type": "object", + "description": "The Veteran for whom Power of Attorney is being requested", + "additionalProperties": false, + "required": [ + "firstName", + "middleName", + "lastName", + "participantId" + ], + "properties": { + "firstName": { + "type": "string" + }, + "middleName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string" + }, + "participantId": { + "type": "integer", + "description": "The identifier of the Veteran as executor of this form application process", + "format": "int64", + "nullable": true + } + } + }, + "representative": { + "type": "object", + "description": "The representative to whom this Power of Attorney request is being submitted", + "additionalProperties": false, + "required": [ + "firstName", + "lastName", + "email" + ], + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "claimant": { + "type": "object", + "description": "If applicable, the individual that executed this form application process for the Veteran", + "additionalProperties": false, + "nullable": true, + "required": [ + "firstName", + "lastName", + "participantId", + "relationshipToVeteran" + ], + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "participantId": { + "type": "integer", + "description": "The identifier of the individual as executor of this form application process", + "format": "int64" + }, + "relationshipToVeteran": { + "type": "string" + } + } + }, + "claimantAddress": { + "type": "object", + "description": "The mailing address of the individual as executor of this form application process", + "additionalProperties": false, + "required": [ + "city", + "state", + "zip", + "country", + "militaryPostOffice", + "militaryPostalCode" + ], + "properties": { + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "country": { + "type": "string" + }, + "militaryPostOffice": { + "type": "string", + "nullable": true + }, + "militaryPostalCode": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } + } +} diff --git a/spec/support/vcr_cassettes/claims_api/bgs/manage_representative_service/read_poa_request/nonempty.yml b/spec/support/vcr_cassettes/claims_api/bgs/manage_representative_service/read_poa_request/nonempty.yml new file mode 100644 index 00000000000..0804f91b103 --- /dev/null +++ b/spec/support/vcr_cassettes/claims_api/bgs/manage_representative_service/read_poa_request/nonempty.yml @@ -0,0 +1,163 @@ +--- +http_interactions: +- request: + method: get + uri: "/VDC/ManageRepresentativeService?WSDL" + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.9.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 12 Apr 2024 21:00:30 GMT + Server: + - Apache + X-Frame-Options: + - SAMEORIGIN + Transfer-Encoding: + - chunked + Content-Type: + - text/xml;charset=utf-8 + Strict-Transport-Security: + - max-age=16000000; includeSubDomains; preload; + body: + encoding: UTF-8 + string: |- + + recorded_at: Fri, 12 Apr 2024 21:00:30 GMT +- request: + method: post + uri: "/VDC/ManageRepresentativeService" + body: + encoding: UTF-8 + string: | + + + + + + VAgovAPI + + + 127.0.0.1 + 281 + VAgovAPI + xUid + xKey + + + + + + + new + pending + accepted + declined + + + 012 + + + + + headers: + User-Agent: + - Faraday v2.9.0 + Content-Type: + - text/xml;charset=UTF-8 + Host: + - ".vba.va.gov" + Soapaction: + - '"readPOARequest"' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 12 Apr 2024 21:00:31 GMT + Server: + - Apache + X-Frame-Options: + - SAMEORIGIN + Transfer-Encoding: + - chunked + Content-Type: + - text/xml; charset=utf-8 + Strict-Transport-Security: + - max-age=16000000; includeSubDomains; preload; + body: + encoding: UTF-8 + string: | + + + + + + + + primero.ultimo@vsorg.org + Primero + Ultimo + Y + Baltimore + US + + + MD + 21218 + Alpha + Omega + 23456 + Cousin + 2024-04-09T23:51:12-05:00 + 2024-04-09T23:51:12-05:00 + Because I felt like it + N + 012 + 12345 + Declined + Firstus + Lastus + + 600043200 + + 1 + + + + + recorded_at: Fri, 12 Apr 2024 21:00:32 GMT +recorded_with: VCR 6.2.0