diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..69f4e6059e0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,64 @@ +{ + "name": "vets-api native setup", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + + "features": { + "ghcr.io/devcontainers-contrib/features/ruby-asdf:0": { + "version": "3.2.3" + }, + "ghcr.io/robbert229/devcontainer-features/postgresql-client:1": { + "version": "15" + }, + "ghcr.io/devcontainers-contrib/features/redis-homebrew:1": { + "version": "6.2" + } + }, + + "forwardPorts": [ + 3000, + 9293, + 5432, + 6379 + ], + "portsAttributes": { + "3000": { + "label": "vets-api", + "onAutoForward": "notify", + "requireLocalPort": true + }, + "9293": { + "label": "vets-api-healthcheck", + "onAutoForward": "silent", + "requireLocalPort": true + }, + "5432": { + "label": "postgis", + "onAutoForward": "silent", + "requireLocalPort": true + }, + "6379": { + "label": "redis", + "onAutoForward": "silent", + "requireLocalPort": true + } + }, + + "postCreateCommand": "sh .devcontainer/post-create.sh", + "postStartCommand": "sh .devcontainer/post-start.sh", + + "customizations": { + "codespaces": { + "repositories": { + "department-of-veterans-affairs/vets-api-mockdata": { + "permissions": { + "contents": "read", + "pull_requests": "write" + } + } + } + }, + "vscode": { + "extensions": ["ms-azuretools.vscode-docker", "Shopify.ruby-lsp"] + } + } +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000000..b01f08ae3ab --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# Add welcome message +sudo cp .devcontainer/welcome.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt + +# Switch to vets-api ruby version +export PATH="${HOME}/.asdf/shims:${HOME}/.asdf/bin:${PATH}" +asdf install ruby $( cat .ruby-version ) +asdf global ruby $( cat .ruby-version ) + +git clone https://github.com/department-of-veterans-affairs/vets-api-mockdata.git ../vets-api-mockdata + +sudo apt update +sudo apt install -y libpq-dev pdftk shared-mime-info postgresql-15-postgis-3 + +gem install bundler +NUM_CORES=$( cat /proc/cpuinfo | grep '^processor'|wc -l ) +bundle config --global jobs `expr $NUM_CORES - 1` + +# Update test DB config +echo 'test_database_url: postgis://postgres:password@localhost:5432/vets_api_test?pool=4' > config/settings/test.local.yml + +# Add service config +if [ ! -f config/settings.local.yml ]; then + cp config/settings.local.yml.example config/settings.local.yml + cat <> config/settings.local.yml +database_url: postgis://postgres:password@localhost:5432/vets_api_development?pool=4 +test_database_url: postgis://postgres:password@localhost:5432/vets_api_test?pool=4 + +redis: + host: localhost + port: 6379 + app_data: + url: redis://localhost:6379 + sidekiq: + url: redis://localhost:6379 + +betamocks: + cache_dir: ../vets-api-mockdata + +# Allow access from localhost and shared github URLs. +virtual_hosts: ["127.0.0.1", "localhost", !ruby/regexp /.*\.app\.github\.dev/] +EOT +fi + +mkdir -p log +nohup bash -c '/home/linuxbrew/.linuxbrew/opt/redis@6.2/bin/redis-server /home/linuxbrew/.linuxbrew/etc/redis.conf' >> log/redis.log 2>&1 & +sudo /etc/init.d/postgresql restart +pg_isready -t 60 +sudo -u root sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'password';" +./bin/setup diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100644 index 00000000000..0abf46ab86d --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "Starting redis..." +nohup /home/linuxbrew/.linuxbrew/opt/redis@6.2/bin/redis-server /home/linuxbrew/.linuxbrew/etc/redis.conf >> log/redis.log 2>&1 & + +echo "Starting postgres..." +sudo /etc/init.d/postgresql restart +echo "Waiting for postgres to be ready..." +pg_isready -t 60 diff --git a/.devcontainer/welcome.txt b/.devcontainer/welcome.txt new file mode 100644 index 00000000000..0e846774f27 --- /dev/null +++ b/.devcontainer/welcome.txt @@ -0,0 +1,9 @@ +~~~~~~ Welcome to vets-api on codespaces! ~~~~~~ + +For more information, see the codespaces README in docs/setup. + +~~~~~~ Quickstart ~~~~~ + +To start vets-api, run this command: + +foreman start -m all=1,clamd=0,freshclam=0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ab300977b7c..271f77d0628 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ Gemfile @department-of-veterans-affairs/backend-review-group Gemfile.lock @department-of-veterans-affairs/backend-review-group Jenkinsfile @department-of-veterans-affairs/backend-review-group Makefile @department-of-veterans-affairs/backend-review-group +.devcontainer @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/cto-engineers app/controllers/appeals_base_controller.rb @department-of-veterans-affairs/backend-review-group app/controllers/appeals_base_controller_v1.rb @department-of-veterans-affairs/backend-review-group app/controllers/application_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -777,6 +778,7 @@ docs/setup/native.md @department-of-veterans-affairs/backend-review-group @depar docs/setup/rswag_setup.md @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers docs/setup/running_docker.md @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers docs/setup/running_natively.md @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers +docs/setup/codespaces.md @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/cto-engineers docs/setup/va_forms.md @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers docs/setup/virtual_machine_access.md @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers .github @department-of-veterans-affairs/backend-review-group diff --git a/Gemfile.lock b/Gemfile.lock index 81f9fedd907..419545d490d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -405,7 +405,7 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.2.3) + faker (3.3.0) i18n (>= 1.8.11, < 2) fakeredis (0.9.2) redis (~> 4.8) @@ -683,7 +683,7 @@ GEM os (1.1.4) ox (2.14.18) parallel (1.24.0) - parallel_tests (4.5.2) + parallel_tests (4.6.0) parallel parser (3.3.0.5) ast (~> 2.4.1) @@ -802,7 +802,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) rchardet (1.8.0) - rdoc (6.6.2) + rdoc (6.6.3.1) psych (>= 4.0.0) redis (4.8.1) redis-namespace (1.11.0) diff --git a/README.md b/README.md index 6fda13c1f7a..ec3b03f5948 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ For frontend, see [vets-website](https://github.com/department-of-veterans-affai - [Native setup](docs/setup/native.md) (OSX/Ubuntu) - [Docker setup](docs/setup/docker.md) - [Hybrid setup](docs/setup/hybrid.md) + - [Codespaces setup](docs/setup/codespaces.md) ## Running the app diff --git a/app/controllers/concerns/headers.rb b/app/controllers/concerns/headers.rb index 650763d8183..33987f5f7a3 100644 --- a/app/controllers/concerns/headers.rb +++ b/app/controllers/concerns/headers.rb @@ -3,13 +3,7 @@ module Headers extend ActiveSupport::Concern - included { prepend_before_action :block_unknown_hosts, :set_app_info_headers } - - # returns a Bad Request if the incoming host header is unsafe. - def block_unknown_hosts - return if controller_name == 'example' - raise Common::Exceptions::NotASafeHostError, request.host unless Settings.virtual_hosts.include?(request.host) - end + included { prepend_before_action :set_app_info_headers } def set_app_info_headers headers['X-Git-SHA'] = AppInfo::GIT_REVISION diff --git a/app/controllers/v0/burial_claims_controller.rb b/app/controllers/v0/burial_claims_controller.rb index 56b202efb03..77d398fb6b5 100644 --- a/app/controllers/v0/burial_claims_controller.rb +++ b/app/controllers/v0/burial_claims_controller.rb @@ -8,18 +8,24 @@ class BurialClaimsController < ClaimsBaseController def create PensionBurial::TagSentry.tag_sentry - claim = claim_class.new(form: filtered_params[:form]) + + claim = if Flipper.enabled?(:va_burial_v2) + # cannot parse a nil form, to pass unit tests do a check for form presence + form = filtered_params[:form] + claim_class.new(form:, formV2: form.present? ? JSON.parse(form)['formV2'] : nil) + else + claim_class.new(form: filtered_params[:form]) + end unless claim.save StatsD.increment("#{stats_key}.failure") Sentry.set_tags(team: 'benefits-memorial-1') # tag sentry logs with team name raise Common::Exceptions::ValidationErrors, claim end - # this method also calls claim.process_attachments! claim.submit_to_structured_data_services! - Rails.logger.info "ClaimID=#{claim.confirmation_number} Form=#{claim.class::FORM}" + Rails.logger.info "ClaimID=#{claim.confirmation_number} Form=#{claim.form_id}" clear_saved_form(claim.form_id) render(json: claim) end diff --git a/app/controllers/v0/claim_documents_controller.rb b/app/controllers/v0/claim_documents_controller.rb index 1bcaba7a14c..95c471e4e66 100644 --- a/app/controllers/v0/claim_documents_controller.rb +++ b/app/controllers/v0/claim_documents_controller.rb @@ -31,7 +31,7 @@ def create def klass case form_id - when '21P-527EZ', '21P-530' + when '21P-527EZ', '21P-530', '21P-530V2' PensionBurial::TagSentry.tag_sentry PersistentAttachments::PensionBurial when '21-686C', '686C-674' diff --git a/app/controllers/v0/pension_claims_controller.rb b/app/controllers/v0/pension_claims_controller.rb index 79f039d112d..d84178c373c 100644 --- a/app/controllers/v0/pension_claims_controller.rb +++ b/app/controllers/v0/pension_claims_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'pension_21p527ez/tag_sentry' + module V0 class PensionClaimsController < ClaimsBaseController service_tag 'pension-application' @@ -45,7 +47,7 @@ def show # Creates and validates an instance of the class, removing any copies of # the form that had been previously saved by the user. def create - PensionBurial::TagSentry.tag_sentry + Pension21p527ez::TagSentry.tag_sentry claim = claim_class.new(form: filtered_params[:form]) user_uuid = current_user&.uuid diff --git a/app/models/form_profile.rb b/app/models/form_profile.rb index 6cc8209e578..bfd47678896 100644 --- a/app/models/form_profile.rb +++ b/app/models/form_profile.rb @@ -92,7 +92,7 @@ class FormProfile 22-5495 22-0993 22-0994 FEEDBACK-TOOL 22-10203 22-1990S 22-1990EZ], evss: ['21-526EZ'], hca: %w[1010ez 10-10EZR], - pension_burial: %w[21P-530 21P-527EZ], + pension_burial: %w[21P-530 21P-527EZ 21P-530V2], dependents: ['686C-674'], decision_review: %w[20-0995 20-0996 10182], mdot: ['MDOT'], @@ -121,6 +121,7 @@ class FormProfile '22-5490E' => ::FormProfiles::VA5490e, '22-5495' => ::FormProfiles::VA5495, '21P-530' => ::FormProfiles::VA21p530, + '21P-530V2' => ::FormProfiles::VA21p530v2, '21-686C' => ::FormProfiles::VA21686c, '686C-674' => ::FormProfiles::VA686c674, '40-10007' => ::FormProfiles::VA4010007, diff --git a/app/models/form_profiles/va_21p530v2.rb b/app/models/form_profiles/va_21p530v2.rb new file mode 100644 index 00000000000..4cdbdfe237e --- /dev/null +++ b/app/models/form_profiles/va_21p530v2.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'iso_country_codes' + +class FormProfiles::VA21p530v2 < FormProfile + def metadata + { + version: 0, + prefill: true, + returnUrl: '/claimant-information' + } + end + + def prefill + @identity_information = initialize_identity_information + @contact_information = initialize_contact_information + if @contact_information&.address&.country.present? + @contact_information.address.country = convert_to_iso2(@contact_information.address.country) + end + mappings = self.class.mappings_for_form(form_id) + + form_data = generate_prefill(mappings) if FormProfile.prefill_enabled_forms.include?(form_id) + + { form_data:, metadata: } + end + + private + + def convert_to_iso2(country_code) + code = IsoCountryCodes.find(country_code) + code.alpha2 + end +end diff --git a/app/models/saved_claim.rb b/app/models/saved_claim.rb index 83c812e08cf..1d73e4da0c2 100644 --- a/app/models/saved_claim.rb +++ b/app/models/saved_claim.rb @@ -31,7 +31,7 @@ class SavedClaim < ApplicationRecord # create a uuid for this second (used in the confirmation number) and store # the form type based on the constant found in the subclass. after_initialize do - self.form_id = self.class::FORM.upcase + self.form_id = self.class::FORM.upcase unless instance_of?(SavedClaim::Burial) end def self.add_form_and_validation(form_id) @@ -51,7 +51,7 @@ def process_attachments! def submit_to_structured_data_services! # Only 21P-530 burial forms are supported at this time - if form_id != '21P-530' + unless %w[21P-530 21P-530V2].include?(form_id) err_message = "Unsupported form id: #{form_id}" raise Common::Exceptions::UnprocessableEntity.new(detail: err_message), err_message end diff --git a/app/models/saved_claim/burial.rb b/app/models/saved_claim/burial.rb index 02bddbc08aa..465821ab084 100644 --- a/app/models/saved_claim/burial.rb +++ b/app/models/saved_claim/burial.rb @@ -5,6 +5,17 @@ class SavedClaim::Burial < CentralMailClaim FORM = '21P-530' + # attribute name is passed from the FE as a flag, maintaining camel case + attr_accessor :formV2 # rubocop:disable Naming/MethodName + + after_initialize do + self.form_id = if Flipper.enabled?(:va_burial_v2) + formV2 || form_id == '21P-530V2' ? '21P-530V2' : self.class::FORM.upcase + else + self.class::FORM.upcase + end + end + def process_attachments! refs = attachment_keys.map { |key| Array(open_struct_form.send(key)) }.flatten files = PersistentAttachment.where(guid: refs.map(&:confirmationCode)) @@ -25,6 +36,14 @@ def email parsed_form['claimantEmail'] end + def form_matches_schema + return unless form_is_string + + JSON::Validator.fully_validate(VetsJsonSchema::SCHEMAS[form_id], parsed_form).each do |v| + errors.add(:form, v.to_s) + end + end + def business_line 'NCA' end diff --git a/app/sidekiq/lighthouse/pension_benefit_intake_job.rb b/app/sidekiq/lighthouse/pension_benefit_intake_job.rb index e75b27be4f6..1decf91402d 100644 --- a/app/sidekiq/lighthouse/pension_benefit_intake_job.rb +++ b/app/sidekiq/lighthouse/pension_benefit_intake_job.rb @@ -3,6 +3,7 @@ require 'benefits_intake_service/service' require 'central_mail/datestamp_pdf' require 'simple_forms_api_submission/metadata_validator' +require 'pension_21p527ez/tag_sentry' module Lighthouse class PensionBenefitIntakeJob @@ -33,6 +34,8 @@ class PensionBenefitIntakeError < StandardError; end # @param [Integer] saved_claim_id # rubocop:disable Metrics/MethodLength def perform(saved_claim_id) + Pension21p527ez::TagSentry.tag_sentry + @saved_claim_id = saved_claim_id @claim = SavedClaim::Pension.find(saved_claim_id) raise PensionBenefitIntakeError, "Unable to find SavedClaim::Pension #{saved_claim_id}" unless @claim diff --git a/app/sidekiq/structured_data/process_data_job.rb b/app/sidekiq/structured_data/process_data_job.rb index b10191b15b9..c6f2400bf05 100644 --- a/app/sidekiq/structured_data/process_data_job.rb +++ b/app/sidekiq/structured_data/process_data_job.rb @@ -25,8 +25,7 @@ def perform(saved_claim_id) ensure @claim.process_attachments! # upload claim and attachments to Central Mail - send_confirmation_email if @claim.form_id == '21P-530' - + send_confirmation_email if %w[21P-530 21P-530V2].include?(@claim.form_id) # veteran lookup for hit/miss metrics in support of Automation work StatsD.increment("#{stats_key}.success", tags: %W[relationship:#{relationship_type} veteranInMVI:#{veteran&.participant_id}]) diff --git a/config/features.yml b/config/features.yml index 2b7e258d616..068190f7557 100644 --- a/config/features.yml +++ b/config/features.yml @@ -1020,10 +1020,6 @@ features: actor_type: user description: Allows veterans to cancel VA appointments enable_in_development: true - va_online_scheduling_clinic_filtering: - actor_type: user - description: Allows clinic selection filtering by stop codes - enable_in_development: true va_online_scheduling_community_care: actor_type: user description: Allows veterans to submit requests for Community Care appointments @@ -1139,6 +1135,9 @@ features: va_view_dependents_access: actor_type: user description: Allows us to gate the View/ Modify dependents content in a progressive rollout + va_burial_v2: + actor_type: user + description: Allows us to toggle between 21-P530 and 21-P530V2 show_edu_benefits_1990EZ_Wizard: actor_type: user description: Navigates user to 1990EZ or 1990 depending on form questions. @@ -1258,6 +1257,9 @@ features: disability_compensation_lighthouse_brd: actor_type: user description: If enabled uses the lighthouse Benefits Reference Data service + disability_compensation_lighthouse_generate_pdf: + actor_type: user + description: If enabled uses the lighthouse Benefits Claims service to generate a 526 pdf virtual_agent_fetch_jwt_token: actor_type: user description: Enable the fetching of a JWT token to access MAP environment @@ -1335,12 +1337,6 @@ features: description: >- Master toggle for the VYE (Verify Your Enrollment) project. If enabled, requests will be allowed to reach the controllers, otherwise a 400 (Bad Request) will be returned. - yellow_ribbon_degree_filter: - actor_type: user - description: Enable the degree type filter for the Find a Yellow Ribbon school search - yellow_ribbon_search_enhancement: - actor_type: user - description: Enable changes to Find a Yellow Ribbon school search functionality travel_pay_power_switch: actor_type: user description: >- diff --git a/docker-compose-deps.yml b/docker-compose-deps.yml index cbf024558b2..b1ab99a4d31 100644 --- a/docker-compose-deps.yml +++ b/docker-compose-deps.yml @@ -1,7 +1,7 @@ version: '3.4' services: redis: - image: redis:5.0-alpine + image: redis:6.2-alpine ports: - "63790:6379" postgres: diff --git a/docker-compose.review.yml b/docker-compose.review.yml index 0c26ee35031..43769083ab7 100644 --- a/docker-compose.review.yml +++ b/docker-compose.review.yml @@ -1,7 +1,7 @@ version: '3.4' services: redis: - image: redis:5.0-alpine + image: redis:6.2-alpine restart: unless-stopped postgres: image: mdillon/postgis:11-alpine diff --git a/docker-compose.yml b/docker-compose.yml index 0a3196fbb65..bf46aa633b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.4' services: redis: - image: redis:5.0-alpine + image: redis:6.2-alpine ports: - "63790:6379" postgres: diff --git a/docs/setup/codespaces.md b/docs/setup/codespaces.md new file mode 100644 index 00000000000..55dda1bc97f --- /dev/null +++ b/docs/setup/codespaces.md @@ -0,0 +1,27 @@ +# Codespaces setup + +## About codespaces + +Github Codespaces provide an Integrated Development Environment (IDE) that is accessible entirely in a web browser. It is essentially a web based version of VS Code running on a cloud based virtual machine. + +Codespaces is available for all members of the Department of Veterans Affairs organization on Github. + +### More information + +- [Platform documentation for codespaces](https://depo-platform-documentation.scrollhelp.site/developer-docs/using-github-codespaces) +- See the #codespaces channel in Slack for additional questions about using Codespaces. + +## Creating a codespace + +1. Go to [your Codespaces page](https://github.com/codespaces) on Github. +2. Click the [new Codespace](https://github.com/codespaces/new) button at the top right. +3. Select the vets-api repository and adjust other settings as desired, the defaults should work well. +4. Click the 'Create codespace' button + +Your new codespace will open in Visual Studio Code if you have it installed locally, or otherwise in the browser. The vets-api repo and all dependencies will be installed, and it will be ready for use in about 5 minutes. + +## Using your codespace + +Your codespace will automatically start vets-api and forward port 3000 to your local machine if you have Visual Studio Code installed. The API can be accessed at http://localhost:3000/ + +For more information on running vets-api and specs, see the [native running instructions](running_natively.md). diff --git a/docs/setup/native.md b/docs/setup/native.md index 62928893891..a01b152f3a4 100644 --- a/docs/setup/native.md +++ b/docs/setup/native.md @@ -3,8 +3,8 @@ Vets API requires: - Ruby 3.2.3 -- PostgreSQL 11.x (including PostGIS 2.5) -- Redis 5.0.x +- PostgreSQL 15.x (including PostGIS 3) +- Redis 6.2.x The most up-to-date versions of each key dependency will be specified in the `docker-compose.yml` [file](https://github.com/department-of-veterans-affairs/vets-api/blob/master/docker-compose.yml) and the `Dockerfile`. @@ -114,14 +114,14 @@ All of the OSX instructions assume `homebrew` is your [package manager](https:// 1. It is MUCH easier to use the [Postgres.app](https://postgresapp.com/downloads.html) which installs the correct combination of Postgresql and PostGIS versions. - - Download the Postgres.app with PostgreSQL 10, 11 and 12 + - Download the Postgres.app with PostgreSQL 15 - Install Instructions here: https://postgresapp.com/ - `sudo mkdir -p /etc/paths.d && echo /Applications/Postgres.app/Contents/Versions/latest/bin | sudo tee /etc/paths.d/postgresapp` - `ARCHFLAGS="-arch x86_64" gem install pg -v 1.2.3` - 2. Alternatively Postgresql 11 & PostGIS 2.5 can be installed with homebrew - - `brew install postgresql@11` - - `brew services start postgresql@11` - - Install the `pex` manager to add your Postgresql 11 extensions from [here](https://github.com/petere/pex#installation) + 2. Alternatively Postgresql 15 & PostGIS 3 can be installed with homebrew + - `brew install postgresql@15` + - `brew services start postgresql@15` + - Install the `pex` manager to add your Postgresql 15 extensions from [here](https://github.com/petere/pex#installation) - Install the `postgis` extension along with a number of patches using the instructions summarized [here](https://gist.github.com/skissane/0487c097872a7f6d0dcc9bcd120c2ccd): - ```bash PG_CPPFLAGS='-DACCEPT_USE_OF_DEPRECATED_PROJ_API_H -I/usr/local/include' CFLAGS='-DACCEPT_USE_OF_DEPRECATED_PROJ_API_H -I/usr/local/include' pex install postgis @@ -181,7 +181,7 @@ All of the OSX instructions assume `homebrew` is your [package manager](https:// 2. Install PostGIS ```bash - sudo apt install -y postgresql-11-postgis-2.5 + sudo apt install -y postgresql-15-postgis-3 sudo -i -u postgres createuser postgis_test diff --git a/lib/bip_claims/service.rb b/lib/bip_claims/service.rb index f1d8b5eec13..204809fb1e1 100644 --- a/lib/bip_claims/service.rb +++ b/lib/bip_claims/service.rb @@ -14,7 +14,7 @@ class Service < Common::Client::Base def veteran_attributes(claim) case claim.form_id - when '21P-530' + when '21P-530', '21P-530V2' ssn, full_name, bday = claim.parsed_form.values_at( 'veteranSocialSecurityNumber', 'veteranFullName', diff --git a/lib/common/exceptions/not_a_safe_host_error.rb b/lib/common/exceptions/not_a_safe_host_error.rb deleted file mode 100644 index f1aaaeae1cd..00000000000 --- a/lib/common/exceptions/not_a_safe_host_error.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'common/exceptions/base_error' -require 'common/exceptions/serializable_error' - -module Common - module Exceptions - # Parameter Missing - required parameter was not provided - class NotASafeHostError < BaseError - attr_reader :host - - def initialize(host) - @host = host - end - - def errors - Array(SerializableError.new(i18n_interpolated(detail: { host: @host }))) - end - end - end -end diff --git a/lib/decision_review/utilities/pdf_validation/configuration.rb b/lib/decision_review/utilities/pdf_validation/configuration.rb index 80f93c24878..75961e382b0 100644 --- a/lib/decision_review/utilities/pdf_validation/configuration.rb +++ b/lib/decision_review/utilities/pdf_validation/configuration.rb @@ -4,7 +4,7 @@ module DecisionReview module PdfValidation class Configuration < DecisionReview::Configuration ## - # @return [String] Base path for decision review URLs. + # @return [String] Base path for PDF validation URL. # def base_path Settings.decision_review.pdf_validation.url @@ -17,6 +17,19 @@ def service_name 'DecisionReview::PDFValidation' end + ## + # @return [Hash] The basic headers required for any decision review API call. + # + def self.base_request_headers + # Can use regular Decision Reviews API key in lower environments + return super unless Rails.env.production? + + # Since we're using the `uploads/validate_document` endpoint under Benefits Intake API, + # we need to use their API key. This is pulled from BenefitsIntakeService::Configuration + api_key = Settings.benefits_intake_service.api_key || Settings.form526_backup.api_key + super.merge('apiKey' => api_key) + end + ## # Creates the a connection with parsing json and adding breakers functionality. # diff --git a/lib/disability_compensation/factories/api_provider_factory.rb b/lib/disability_compensation/factories/api_provider_factory.rb index e9d7b1913b7..a8b7185306e 100644 --- a/lib/disability_compensation/factories/api_provider_factory.rb +++ b/lib/disability_compensation/factories/api_provider_factory.rb @@ -15,6 +15,9 @@ require 'disability_compensation/providers/brd/brd_provider' require 'disability_compensation/providers/brd/evss_brd_provider' require 'disability_compensation/providers/brd/lighthouse_brd_provider' +require 'disability_compensation/providers/generate_pdf/generate_pdf_provider' +require 'disability_compensation/providers/generate_pdf/evss_generate_pdf_provider' +require 'disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider' require 'logging/third_party_transaction' class ApiProviderFactory @@ -31,7 +34,8 @@ class UndefinedFactoryTypeError < StandardError; end intent_to_file: :intent_to_file, ppiu: :ppiu, claims: :claims, - brd: :brd + brd: :brd, + generate_pdf: :generate_pdf }.freeze # Splitting the rated disabilities functionality into two use cases: @@ -47,6 +51,7 @@ class UndefinedFactoryTypeError < StandardError; end # PPIU calls out to Direct Deposit APIs in Lighthouse FEATURE_TOGGLE_PPIU_DIRECT_DEPOSIT = 'disability_compensation_lighthouse_ppiu_direct_deposit_provider' FEATURE_TOGGLE_BRD = 'disability_compensation_lighthouse_brd' + FEATURE_TOGGLE_GENERATE_PDF = 'disability_compensation_lighthouse_generate_pdf' attr_reader :type @@ -56,6 +61,7 @@ class UndefinedFactoryTypeError < StandardError; end :ppiu_service_provider, :claims_service_provider, :brd_service_provider, + :generate_pdf_service_provider, additional_class_logs: { action: 'disability compensation factory choosing API Provider' }, @@ -91,6 +97,8 @@ def call claims_service_provider when FACTORIES[:brd] brd_service_provider + when FACTORIES[:generate_pdf] + generate_pdf_service_provider else raise UndefinedFactoryTypeError end @@ -153,6 +161,23 @@ def brd_service_provider end end + def generate_pdf_service_provider + case api_provider + when API_PROVIDER[:evss] + if @options[:auth_headers].nil? || @options[:auth_headers]&.empty? + raise StandardError, 'options[:auth_headers] is required to create a generate an EVSS pdf provider' + end + + # provide options[:breakered] = false if this needs to use the non-breakered configuration + # for instance, in the backup process + EvssGeneratePdfProvider.new(@options[:auth_headers], breakered: @options[:breakered]) + when API_PROVIDER[:lighthouse] + LighthouseGeneratePdfProvider.new({}) + else + raise NotImplementedError, 'No known Generate Pdf Api Provider type provided' + end + end + def api_provider @api_provider ||= if Flipper.enabled?(@feature_toggle, @current_user) API_PROVIDER[:lighthouse] diff --git a/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider.rb b/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider.rb new file mode 100644 index 00000000000..6cd03f26212 --- /dev/null +++ b/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'disability_compensation/providers/generate_pdf/generate_pdf_provider' +require 'evss/disability_compensation_form/service' +require 'evss/disability_compensation_form/non_breakered_service' + +class EvssGeneratePdfProvider + include GeneratePdfProvider + + def initialize(auth_headers, breakered: true) + # both of these services implement `get_form526` + @service = if breakered + EVSS::DisabilityCompensationForm::Service.new(auth_headers) + else + EVSS::DisabilityCompensationForm::NonBreakeredService.new(auth_headers) + end + end + + def generate_526_pdf(form_content) + @service.get_form526(form_content) + end +end diff --git a/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider.rb b/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider.rb new file mode 100644 index 00000000000..f914f6c9772 --- /dev/null +++ b/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module GeneratePdfProvider + def self.generate_526_pdf(_form_content) + raise NotImplementedError, 'Do not use base module methods. Override this method in implementation class.' + end +end diff --git a/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider.rb b/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider.rb new file mode 100644 index 00000000000..0384339de2d --- /dev/null +++ b/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'disability_compensation/providers/generate_pdf/generate_pdf_provider' + +class LighthouseGeneratePdfProvider + include GeneratePdfProvider + + def initialize(_auth_headers) + # TODO: Implement in Ticket# + end + + def generate_526_pdf(_form_content) + # TODO: Implement in Ticket# + end +end diff --git a/lib/evss/disability_compensation_form/form4142_processor.rb b/lib/evss/disability_compensation_form/form4142_processor.rb index 07cef2a5465..446f5d55349 100644 --- a/lib/evss/disability_compensation_form/form4142_processor.rb +++ b/lib/evss/disability_compensation_form/form4142_processor.rb @@ -2,6 +2,7 @@ require 'pdf_fill/forms/va21p527ez' require 'pdf_fill/forms/va21p530' +require 'pdf_fill/forms/va21p530v2' require 'pdf_fill/forms/va214142' require 'pdf_fill/forms/va210781a' require 'pdf_fill/forms/va210781' diff --git a/lib/pdf_fill/filler.rb b/lib/pdf_fill/filler.rb index 69122f4c6cc..2ce6719beca 100644 --- a/lib/pdf_fill/filler.rb +++ b/lib/pdf_fill/filler.rb @@ -2,6 +2,7 @@ require 'pdf_fill/forms/va21p527ez' require 'pdf_fill/forms/va21p530' +require 'pdf_fill/forms/va21p530v2' require 'pdf_fill/forms/va214142' require 'pdf_fill/forms/va210781a' require 'pdf_fill/forms/va210781' @@ -25,6 +26,7 @@ module Filler FORM_CLASSES = { '21P-527EZ' => PdfFill::Forms::Va21p527ez, '21P-530' => PdfFill::Forms::Va21p530, + '21P-530V2' => PdfFill::Forms::Va21p530v2, '21-4142' => PdfFill::Forms::Va214142, '21-0781a' => PdfFill::Forms::Va210781a, '21-0781' => PdfFill::Forms::Va210781, diff --git a/lib/pdf_fill/forms/pdfs/21P-530V2.pdf b/lib/pdf_fill/forms/pdfs/21P-530V2.pdf new file mode 100644 index 00000000000..87650f0b3d5 Binary files /dev/null and b/lib/pdf_fill/forms/pdfs/21P-530V2.pdf differ diff --git a/lib/pdf_fill/forms/va21p530v2.rb b/lib/pdf_fill/forms/va21p530v2.rb new file mode 100644 index 00000000000..b850d80a404 --- /dev/null +++ b/lib/pdf_fill/forms/va21p530v2.rb @@ -0,0 +1,732 @@ +# frozen_string_literal: true + +require 'pdf_fill/hash_converter' +require 'pdf_fill/forms/form_base' +require 'pdf_fill/forms/form_helper' +require 'string_helpers' + +# rubocop:disable Metrics/ClassLength +module PdfFill + module Forms + class Va21p530v2 < FormBase + include FormHelper + + ITERATOR = PdfFill::HashConverter::ITERATOR + + PLACE_OF_DEATH_KEY = { + 'vaMedicalCenter' => 'VA MEDICAL CENTER', + 'stateVeteransHome' => 'STATE VETERANS HOME', + 'nursingHome' => 'NURSING HOME UNDER VA CONTRACT' + }.freeze + + # rubocop:disable Layout/LineLength + KEY = { + 'veteranFullName' => { # start veteran information + 'first' => { + key: 'form1[0].#subform[82].VeteransFirstName[0]', + limit: 12, + question_num: 1, + question_text: "DECEASED VETERAN'S FIRST NAME" + }, + 'middle' => { + key: 'form1[0].#subform[82].VeteransMiddleInitial1[0]', + question_num: 1, + limit: 1, + question_text: "DECEASED VETERAN'S MIDDLE INITIAL" + }, + 'last' => { + key: 'form1[0].#subform[82].VeteransLastName[0]', + limit: 18, + question_num: 1, + question_text: "DECEASED VETERAN'S LAST NAME" + }, + 'suffix' => { + key: 'form1[0].#subform[82].Suffix[0]', + question_num: 1, + limit: 0, + question_text: "DECEASED VETERAN'S SUFFIX" + } + }, + 'veteranSocialSecurityNumber' => { + 'first' => { + key: 'form1[0].#subform[82].VeteransSocialSecurityNumber_FirstThreeNumbers[0]' + }, + 'second' => { + key: 'form1[0].#subform[82].VeteransSocialSecurityNumber_SecondTwoNumbers[0]' + }, + 'third' => { + key: 'form1[0].#subform[82].VeteransSocialSecurityNumber_LastFourNumbers[0]' + } + }, + 'vaFileNumber' => { + key: 'form1[0].#subform[82].VAFileNumber[0]', + question_num: 3 + }, + 'veteranDateOfBirth' => { + 'month' => { + key: 'form1[0].#subform[82].Veterans_DOBmonth[0]', + limit: 2, + question_num: 4, + question_suffix: 'A', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BIRTH (MM-DD-YYYY)' + }, + 'day' => { + key: 'form1[0].#subform[82].Veterans_DOBday[0]', + limit: 2, + question_num: 4, + question_suffix: 'B', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BIRTH (MM-DD-YYYY)' + }, + 'year' => { + key: 'form1[0].#subform[82].Veterans_DOByear[0]', + limit: 4, + question_num: 4, + question_suffix: 'C', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BIRTH (MM-DD-YYYY)' + } + }, + 'deathDate' => { + 'month' => { + key: 'form1[0].#subform[82].Veterans_DateOfDeathmonth[0]', + limit: 2, + question_num: 5, + question_suffix: 'A', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF DEATH (MM-DD-YYYY)' + }, + 'day' => { + key: 'form1[0].#subform[82].Veterans_DateofDeathday[0]', + limit: 2, + question_num: 5, + question_suffix: 'B', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF DEATH (MM-DD-YYYY)' + }, + 'year' => { + key: 'form1[0].#subform[82].Veterans_DateofDeathyear[0]', + limit: 4, + question_num: 5, + question_suffix: 'C', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF DEATH (MM-DD-YYYY)' + } + }, + 'burialDate' => { + 'month' => { + key: 'form1[0].#subform[82].Veterans_Date_of_Burial_Month[0]', + limit: 2, + question_num: 6, + question_suffix: 'A', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BURIAL (MM-DD-YYYY)' + }, + 'day' => { + key: 'form1[0].#subform[82].Veterans_Date_of_Burial_Day[0]', + limit: 2, + question_num: 6, + question_suffix: 'B', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BURIAL (MM-DD-YYYY)' + }, + 'year' => { + key: 'form1[0].#subform[82].Veterans_Date_of_Burial_Year[0]', + limit: 4, + question_num: 6, + question_suffix: 'C', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > VETERAN\'S DATE OF BURIAL (MM-DD-YYYY)' + } + }, # end veteran information + 'claimantFullName' => { # start claimant information + 'first' => { + key: 'form1[0].#subform[82].ClaimantsFirstName[0]', + limit: 12, + question_num: 7, + question_text: "CLAIMANT'S FIRST NAME" + }, + 'middle' => { + key: 'form1[0].#subform[82].ClaimantsMiddleInitial1[0]' + }, + 'last' => { + key: 'form1[0].#subform[82].ClaimantsLastName[0]', + limit: 18, + question_num: 7, + question_text: "CLAIMANT'S LAST NAME" + }, + 'suffix' => { + key: 'form1[0].#subform[82].ClaimantSuffix[0]', + question_num: 7, + limit: 0, + question_text: "CLAIMANT'S SUFFIX" + } + }, + 'claimantSocialSecurityNumber' => { + 'first' => { + key: 'form1[0].#subform[82].Claimants_SocialSecurityNumber_FirstThreeNumbers[0]' + }, + 'second' => { + key: 'form1[0].#subform[82].Claimants_SocialSecurityNumber_SecondTwoNumbers[0]' + }, + 'third' => { + key: 'form1[0].#subform[82].Claimants_SocialSecurityNumber_LastFourNumbers[0]' + } + }, + 'claimantDateOfBirth' => { + 'month' => { + key: 'form1[0].#subform[82].Claimants_DOBmonth[0]', + limit: 2, + question_num: 9, + question_suffix: 'A', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > CLAIMANT\'S DATE OF BIRTH (MM-DD-YYYY)' + }, + 'day' => { + key: 'form1[0].#subform[82].Claimants_DOBday[0]', + limit: 2, + question_num: 9, + question_suffix: 'B', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > CLAIMANT\'S DATE OF BIRTH (MM-DD-YYYY)' + }, + 'year' => { + key: 'form1[0].#subform[82].Claimants_DOByear[0]', + limit: 4, + question_num: 9, + question_suffix: 'C', + question_text: 'VETERAN/CLAIMANT\'S IDENTIFICATION INFORMATION > CLAIMANT\'S DATE OF BIRTH (MM-DD-YYYY)' + } + }, + 'claimantAddress' => { + 'street' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_NumberAndStreet[0]', + limit: 30, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - STREET" + }, + 'street2' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_ApartmentOrUnitNumber[0]', + limit: 5, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - APT/UNIT NO." + }, + 'city' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_City[0]', + limit: 18, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - CITY" + }, + 'state' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_StateOrProvince[0]', + limit: 2, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - STATE" + }, + 'country' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_Country[0]', + limit: 2, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - COUNTRY" + }, + 'postalCode' => { + 'firstFive' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_ZIPOrPostalCode_FirstFiveNumbers[0]', + limit: 5, + question_num: 10, + question_text: "CLAIMANT'S ADDRESS - POSTAL CODE - FIRST FIVE" + }, + 'lastFour' => { + key: 'form1[0].#subform[82].CurrentMailingAddress_ZIPOrPostalCode_LastFourNumbers[0]', + limit: 4, + question: 10, + question_text: "CLAIMANT's ADDRESS - POSTAL CODE - LAST FOUR" + } + } + }, + 'claimantPhone' => { + 'first' => { + key: 'form1[0].#subform[82].TelephoneNumber_AreaCode[0]' + }, + 'second' => { + key: 'form1[0].#subform[82].TelephoneNumber_FirstThreeNumbers[0]' + }, + 'third' => { + key: 'form1[0].#subform[82].TelephoneNumber_LastFourNumbers[0]' + } + }, + 'claimantIntPhone' => { + key: 'form1[0].#subform[82].IntTelephoneNumber[0]', + question_num: 11, + question_text: "CLAIMANT'S INTERNATIONAL PHONE NUMBER", + limit: 0 # this will force this value that is not on the pdf to appear in the overflow + }, + 'claimantEmail' => { + key: 'form1[0].#subform[82].E-Mail_Address[0]', + limit: 31, + question_num: 12, + question_text: 'E-MAIL ADDRESS' + }, + 'relationshipToVeteran' => { + 'spouse' => { + key: 'form1[0].#subform[82].CheckboxSpouse[0]' + }, + 'child' => { + key: 'form1[0].#subform[82].CheckboxChild[0]' + }, + 'parent' => { + key: 'form1[0].#subform[82].CheckboxParent[0]' + }, + 'executor' => { + key: 'form1[0].#subform[82].CheckboxExecutor[0]' + }, + 'funeralHome' => { + key: 'form1[0].#subform[82].CheckboxFuneralHome[0]' + }, + 'other' => { + key: 'form1[0].#subform[82].CheckboxOther[0]' + } + }, + 'toursOfDuty' => { + limit: 3, + first_key: 'rank', + 'dateRangeStart' => { + key: "form1[0].#subform[82].DATE_ENTERED_SERVICE[#{ITERATOR}]", + question_num: 14, + question_suffix: 'A', + question_text: 'ENTERED SERVICE (date)', + format: 'date' + }, + 'placeOfEntry' => { + key: "form1[0].#subform[82].PLACE[#{ITERATOR}]", + limit: 14, + question_num: 14, + question_suffix: 'A', + question_text: 'ENTERED SERVICE (place)' + }, + 'militaryServiceNumber' => { + key: "form1[0].#subform[82].SERVICE_NUMBER[#{ITERATOR}]", + limit: 12, + question_num: 14, + question_suffix: 'B', + question_text: 'SERVICE NUMBER' + }, + 'dateRangeEnd' => { + key: "form1[0].#subform[82].DATE_SEPARATED_SERVICE[#{ITERATOR}]", + question_num: 14, + question_suffix: 'C', + question_text: 'SEPARATED FROM SERVICE (date)', + format: 'date' + }, + 'placeOfSeparation' => { + key: "form1[0].#subform[82].PLACE_SEPARATED[#{ITERATOR}]", + question_num: 14, + question_suffix: 'C', + question_text: 'SEPARATED FROM SERVICE (place)', + limit: 15 + }, + 'rank' => { + key: "form1[0].#subform[82].GRADE_RANK_OR_RATING[#{ITERATOR}]", + question_num: 11, + question_suffix: 'D', + question_text: 'GRADE, RANK OR RATING, ORGANIZATION AND BRANCH OF SERVICE', + limit: 31 + } + }, + 'previousNames' => { + key: 'form1[0].#subform[82].OTHER_NAME_VETERAN_SERVED_UNDER[0]', + question_num: 15, + question_text: 'IF VETERAN SERVED UNDER NAME OTHER THAN THAT SHOWN IN ITEM 1, GIVE FULL NAME AND SERVICE RENDERED UNDER THAT NAME', + limit: 180 + }, + 'veteranSocialSecurityNumber2' => { + 'first' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_FirstThreeNumbers[1]' + }, + 'second' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_SecondTwoNumbers[1]' + }, + 'third' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_LastFourNumbers[1]' + } + }, + 'finalRestingPlace' => { # break into yes/nos + 'location' => { + 'cemetery' => { + key: 'form1[0].#subform[83].#subform[84].RestingPlaceCemetery[5]' + }, + 'privateResidence' => { + key: 'form1[0].#subform[83].#subform[84].RestingPlacePrivateResidence[5]' + }, + 'mausoleum' => { + key: 'form1[0].#subform[83].#subform[84].RestingPlaceMausoleum[5]' + }, + 'other' => { + key: 'form1[0].#subform[83].#subform[84].RestingPlaceOther[5]' + } + }, + 'other' => { + limit: 58, + question_num: 16, + question_text: "PLACE OF BURIAL PLOT, INTERMENT SITE, OR FINAL RESTING PLACE OF DECEASED VETERAN'S REMAINS", + key: 'form1[0].#subform[83].#subform[84].PLACE_OF_DEATH[0]' + } + }, + 'hasNationalOrFederal' => { + key: 'form1[0].#subform[37].FederalCemeteryYES[0]' + }, + 'noNationalOrFederal' => { + key: 'form1[0].#subform[37].FederalCemeteryNo[0]' + }, + 'name' => { + key: 'form1[0].#subform[37].FederalCemeteryName[0]', + limit: 50 + }, + 'cemetaryLocationQuestionCemetery' => { + key: 'form1[0].#subform[37].HasStateCemetery[2]' + }, + 'cemetaryLocationQuestionTribal' => { + key: 'form1[0].#subform[37].HasTribalTrust[2]' + }, + 'cemetaryLocationQuestionNone' => { + key: 'form1[0].#subform[37].NoStateCemetery[2]' + }, + 'stateCemeteryOrTribalTrustName' => { + key: 'form1[0].#subform[37].StateCemeteryOrTribalTrustName[2]', + limit: 33 + }, + 'stateCemeteryOrTribalTrustZip' => { + key: 'form1[0].#subform[37].StateCemeteryOrTribalTrustZip[2]' + }, + 'hasGovtContributions' => { + key: 'form1[0].#subform[37].GovContributionYES[0]' + }, + 'noGovtContributions' => { + key: 'form1[0].#subform[37].GovContributionNo[0]' + }, + 'amountGovtContribution' => { + key: 'form1[0].#subform[37].AmountGovtContribution[0]', + question_num: 19, + question_suffix: 'B', + dollar: true, + question_text: 'AMOUNT OF GOVERNMENT OR EMPLOYER CONTRIBUTION', + limit: 5 + }, + 'burialAllowanceRequested' => { + 'checkbox' => { + 'nonService' => { + key: 'form1[0].#subform[83].Non-Service-Connected[0]' + }, + 'service' => { + key: 'form1[0].#subform[83].Service-Connected[0]' + }, + 'unclaimed' => { + key: 'form1[0].#subform[83].UnclaimedRemains[0]' + } + } + }, + 'locationOfDeath' => { + 'checkbox' => { + 'nursingHomeUnpaid' => { + key: 'form1[0].#subform[83].NursingHomeOrResidenceNotPaid[1]' + }, + 'nursingHomePaid' => { + key: 'form1[0].#subform[83].NursingHomeOrResidencePaid[1]' + }, + 'vaMedicalCenter' => { + key: 'form1[0].#subform[83].VaMedicalCenter[1]' + }, + 'stateVeteransHome' => { + key: 'form1[0].#subform[83].StateVeteransHome[1]' + }, + 'other' => { + key: 'form1[0].#subform[83].DeathOccurredOther[1]' + } + }, + 'other' => { + key: 'form1[0].#subform[37].DeathOccurredOtherSpecify[1]', + question_num: 20, + question_suffix: 'B', + question_text: "WHERE DID THE VETERAN'S DEATH OCCUR?", + limit: 32 + } + }, + 'hasPreviouslyReceivedAllowance' => { + key: 'form1[0].#subform[83].PreviousAllowanceYes[0]' + }, + 'noPreviouslyReceivedAllowance' => { + key: 'form1[0].#subform[83].PreviousAllowanceNo[0]' + }, + 'hasBurialExpenseResponsibility' => { + key: 'form1[0].#subform[83].ResponsibleForBurialCostYes[0]' + }, + 'noBurialExpenseResponsibility' => { + key: 'form1[0].#subform[83].ResponsibleForBurialCostNo[0]' + }, + 'hasConfirmation' => { + key: 'form1[0].#subform[83].certifyUnclaimedYes[0]' + }, + 'noConfirmation' => { + key: 'form1[0].#subform[83].certifyUnclaimedNo[0]' + }, + 'hasPlotExpenseResponsibility' => { + key: 'form1[0].#subform[83].ResponsibleForPlotIntermentCostYes[0]' + }, + 'noPlotExpenseResponsibility' => { + key: 'form1[0].#subform[83].ResponsibleForPlotIntermentCostNo[0]' + }, + 'hasTransportation' => { + key: 'form1[0].#subform[83].ResponsibleForTransportationYes[0]' + }, + 'noTransportation' => { + key: 'form1[0].#subform[83].ResponsibleForTransportationNo[0]' + }, + 'hasProcessOption' => { + key: 'form1[0].#subform[83].WantClaimFDCProcessedYes[0]' + }, + 'noProcessOption' => { + key: 'form1[0].#subform[83].WantClaimFDCProcessedNo[0]' + }, + 'signature' => { + key: 'form1[0].#subform[83].CLAIMANT_SIGNATURE[0]', + limit: 45, + question_num: 25, + question_text: 'SIGNATURE OF CLAIMANT', + question_suffix: 'A' + }, + 'claimantPrintedName' => { + key: 'form1[0].#subform[83].ClaimantPrintedName[0]', + limit: 45, + question_num: 25, + question_text: 'Printed Name of Claimant', + question_suffix: 'B' + }, + 'firmNameAndAddr' => { + key: 'form1[0].#subform[83].FirmNameAndAddress[0]', + limit: 90, + question_num: 26, + question_suffix: 'B', + question_text: 'FULL NAME AND ADDRESS OF THE FIRM, CORPORATION, OR STATE AGENCY FILING AS CLAIMANT' + }, + 'officialPosition' => { + key: 'form1[0].#subform[83].OfficialPosition[0]', + limit: 90, + question_num: 26, + question_suffix: 'B', + question_text: 'OFFICIAL POSITION OF PERSON SIGNING ON BEHALF OF FIRM, CORPORATION OR STATE AGENCY' + }, + 'veteranSocialSecurityNumber3' => { + 'first' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_FirstThreeNumbers[2]' + }, + 'second' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_SecondTwoNumbers[2]' + }, + 'third' => { + key: 'form1[0].#subform[83].#subform[84].VeteransSocialSecurityNumber_LastFourNumbers[2]' + } + } + }.freeze + # rubocop:enable Layout/LineLength + + def split_phone(hash, key) + phone = hash[key] + return if phone.blank? + + hash[key] = { + 'first' => phone[0..2], + 'second' => phone[3..5], + 'third' => phone[6..9] + } + end + + def split_postal_code(hash) + postal_code = hash['claimantAddress']['postalCode'] + return if postal_code.blank? + + hash['claimantAddress']['postalCode'] = { + 'firstFive' => postal_code[0..4], + 'lastFour' => postal_code[6..10] + } + end + + # override for how this pdf works, it needs the strings of yes/no + def expand_checkbox(value, key) + { + "has#{key}" => value == true ? 'YES' : nil, + "no#{key}" => value == false ? 'NO' : nil + } + end + + def expand_checkbox_in_place(hash, key) + hash.merge!(expand_checkbox(hash[key], StringHelpers.capitalize_only(key))) + end + + def expand_relationship(hash, key) + expand_checkbox_as_hash(hash[key], 'type') + end + + def expand_tours_of_duty(tours_of_duty) + return if tours_of_duty.blank? + + tours_of_duty.each do |tour_of_duty| + expand_date_range(tour_of_duty, 'dateRange') + tour_of_duty['rank'] = combine_hash(tour_of_duty, %w[serviceBranch rank], ', ') + tour_of_duty['militaryServiceNumber'] = @form_data['militaryServiceNumber'] + end + end + + def convert_location_of_death + location_of_death = @form_data['locationOfDeath'] + return if location_of_death.blank? + + location_of_death['location'] = 'nursingHomeUnpaid' if location_of_death['location'] == 'atHome' + expand_checkbox_as_hash(@form_data['locationOfDeath'], 'location') + end + + def expand_burial_allowance + burial_allowance = @form_data['burialAllowanceRequested'] + return if burial_allowance.blank? + + burial_allowance.each do |key, value| + burial_allowance[key] = value.present? ? 'On' : nil + end + + @form_data['burialAllowanceRequested'] = { + 'checkbox' => burial_allowance + } + end + + def expand_cemetery_location + cemetery_location = @form_data['cemeteryLocation'] + return if cemetery_location.blank? + + @form_data['stateCemeteryOrTribalTrustName'] = cemetery_location['name'] if cemetery_location['name'].present? + @form_data['stateCemeteryOrTribalTrustZip'] = cemetery_location['zip'] if cemetery_location['zip'].present? + end + + # VA file number can be up to 10 digits long; An optional leading 'c' or 'C' followed by + # 7-9 digits. The file number field on the 4142 form has space for 9 characters so trim the + # potential leading 'c' to ensure the file number will fit into the form without overflow. + def extract_va_file_number(va_file_number) + return va_file_number if va_file_number.blank? || va_file_number.length < 10 + + va_file_number.sub(/^[Cc]/, '') + end + + # override for on/off vs 1/off + def select_checkbox(value) + value ? 'On' : 'Off' + end + + # override + def expand_checkbox_as_hash(hash, key) + value = hash.try(:[], key) + return if value.blank? + + hash['checkbox'] = { + value => 'On' + } + end + + def expand_confirmation_question + if @form_data['confirmation'].present? + confirmation = @form_data['confirmation'] + @form_data['confirmation'] = confirmation['checkBox'] + expand_checkbox_in_place(@form_data, 'confirmation') + end + end + + def expand_location_question + cemetery_location = @form_data['cemetaryLocationQuestion'] + @form_data['cemetaryLocationQuestionCemetery'] = select_checkbox(cemetery_location == 'cemetery') + @form_data['cemetaryLocationQuestionTribal'] = select_checkbox(cemetery_location == 'tribal') + @form_data['cemetaryLocationQuestionNone'] = select_checkbox(cemetery_location == 'none') + end + + def combine_previous_names_and_service(previous_names) + return if previous_names.blank? + + previous_names.map do |previous_name| + "#{combine_full_name(previous_name)} (#{previous_name['serviceBranch']})" + end.join('; ') + end + + # rubocop:disable Metrics/MethodLength + def merge_fields(_options = {}) + expand_signature(@form_data['claimantFullName']) + + %w[veteranFullName claimantFullName].each do |attr| + extract_middle_i(@form_data, attr) + end + + %w[veteranDateOfBirth deathDate burialDate claimantDateOfBirth].each do |attr| + @form_data[attr] = split_date(@form_data[attr]) + end + + ssn = @form_data['veteranSocialSecurityNumber'] + ['', '2', '3'].each do |suffix| + @form_data["veteranSocialSecurityNumber#{suffix}"] = split_ssn(ssn) + end + + @form_data['claimantSocialSecurityNumber'] = split_ssn(@form_data['claimantSocialSecurityNumber']) + + relationship_to_veteran = @form_data['relationshipToVeteran'] + @form_data['relationshipToVeteran'] = { + 'spouse' => select_checkbox(relationship_to_veteran == 'spouse'), + 'child' => select_checkbox(relationship_to_veteran == 'child'), + 'executor' => select_checkbox(relationship_to_veteran == 'executor'), + 'parent' => select_checkbox(relationship_to_veteran == 'parent'), + 'funeralHome' => select_checkbox(relationship_to_veteran == 'funeralHome'), + 'other' => select_checkbox(relationship_to_veteran == 'other') + } + + final_resting_place = @form_data.dig('finalRestingPlace', 'location') + @form_data['finalRestingPlace']['location'] = { + 'cemetery' => select_checkbox(final_resting_place == 'cemetery'), + 'privateResidence' => select_checkbox(final_resting_place == 'privateResidence'), + 'mausoleum' => select_checkbox(final_resting_place == 'mausoleum'), + 'other' => select_checkbox(final_resting_place == 'other') + } + + expand_cemetery_location + + # special case: these fields were built as checkboxes instead of radios, so usual radio logic can't be used. + burial_expense_responsibility = @form_data['burialExpenseResponsibility'] + @form_data['hasBurialExpenseResponsibility'] = burial_expense_responsibility ? 'On' : nil + @form_data['noBurialExpenseResponsibility'] = burial_expense_responsibility ? nil : 'On' + + # special case: these fields were built as checkboxes instead of radios, so usual radio logic can't be used. + plot_expense_responsibility = @form_data['plotExpenseResponsibility'] + @form_data['hasPlotExpenseResponsibility'] = plot_expense_responsibility ? 'On' : nil + @form_data['noPlotExpenseResponsibility'] = plot_expense_responsibility ? nil : 'On' + + # special case: these fields were built as checkboxes instead of radios, so usual radio logic can't be used. + process_option = @form_data['processOption'] + @form_data['hasProcessOption'] = process_option ? 'On' : nil + @form_data['noProcessOption'] = process_option ? nil : 'On' + + expand_confirmation_question + expand_location_question + + split_phone(@form_data, 'claimantPhone') + + split_postal_code(@form_data) + + expand_tours_of_duty(@form_data['toursOfDuty']) + + @form_data['previousNames'] = combine_previous_names_and_service(@form_data['previousNames']) + + @form_data['vaFileNumber'] = extract_va_file_number(@form_data['vaFileNumber']) + + expand_burial_allowance + + convert_location_of_death + + %w[ + nationalOrFederal + govtContributions + previouslyReceivedAllowance + allowanceStatementOfTruth + transportation + ].each do |attr| + expand_checkbox_in_place(@form_data, attr) + end + + @form_data + end + # rubocop:enable Metrics/MethodLength + end + end +end +# rubocop:enable Metrics/ClassLength diff --git a/lib/pension_21p527ez/tag_sentry.rb b/lib/pension_21p527ez/tag_sentry.rb new file mode 100644 index 00000000000..d11a5a5876a --- /dev/null +++ b/lib/pension_21p527ez/tag_sentry.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Pension21p527ez + module TagSentry + module_function + + TAG_NAME = 'pension_21p527ez' + + def tag_sentry + Sentry.set_tags(feature: TAG_NAME) + end + end +end diff --git a/lib/sidekiq/form526_backup_submission_process/processor.rb b/lib/sidekiq/form526_backup_submission_process/processor.rb index 0394818197d..1645719f412 100644 --- a/lib/sidekiq/form526_backup_submission_process/processor.rb +++ b/lib/sidekiq/form526_backup_submission_process/processor.rb @@ -13,6 +13,7 @@ require 'pdf_fill/filler' require 'logging/third_party_transaction' require 'simple_forms_api_submission/metadata_validator' +require 'disability_compensation/factories/api_provider_factory' module Sidekiq module Form526BackupSubmissionProcess @@ -341,7 +342,10 @@ def get_form526_pdf end def get_form_from_external_api(headers, form_json) - EVSS::DisabilityCompensationForm::Service.new(headers).get_form526(form_json) + # get the "breakered" version + service = choose_provider(headers, breakered: true) + + service.generate_526_pdf(form_json) end def get_uploads @@ -423,6 +427,18 @@ def convert_doc_to_pdf(doc, klass) Common::FileHelpers.delete_file_if_exists(actual_path_to_file) if ::Rails.env.production? end end + + def choose_provider(headers, breakered: true) + ApiProviderFactory.call( + type: ApiProviderFactory::FACTORIES[:generate_pdf], + # let Flipper - the feature toggle - choose which provider + provider: nil, + # this sends the auth headers and if we want the "breakered" or "non-breakered" version + options: { auth_headers: headers, breakered: }, + current_user: OpenStruct.new({ flipper_id: submission.user_uuid }), + feature_toggle: ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF + ) + end end class NonBreakeredProcessor < Processor @@ -444,7 +460,10 @@ def get_form526_pdf end def get_from_non_breakered_service(headers, form_json) - EVSS::DisabilityCompensationForm::NonBreakeredService.new(headers).get_form526(form_json) + # get the "non-breakered" version + service = choose_provider(headers, breakered: false) + + service.get_form526(form_json) end class NonBreakeredForm526BackgroundLoader diff --git a/lib/statsd_middleware.rb b/lib/statsd_middleware.rb index 9b1f97c3be0..7d54281a381 100644 --- a/lib/statsd_middleware.rb +++ b/lib/statsd_middleware.rb @@ -61,6 +61,7 @@ class StatsdMiddleware avs burial-poc-v6 burials + burials-v2 check-in claims-status coe diff --git a/lib/va_profile/profile/v3/health_benefit_bio_response.rb b/lib/va_profile/profile/v3/health_benefit_bio_response.rb index ffea96f8731..1dde3fb9f89 100644 --- a/lib/va_profile/profile/v3/health_benefit_bio_response.rb +++ b/lib/va_profile/profile/v3/health_benefit_bio_response.rb @@ -4,23 +4,39 @@ require 'va_profile/models/associated_person' require 'va_profile/models/message' -module VAProfile::Profile::V3 - class HealthBenefitBioResponse < VAProfile::Response - attr_reader :body +module VAProfile + module Profile + module V3 + class HealthBenefitBioResponse < VAProfile::Response + attr_reader :body - attribute :contacts, Array[VAProfile::Models::AssociatedPerson] - attribute :messages, Array[VAProfile::Models::Message] + attribute :contacts, Array[VAProfile::Models::AssociatedPerson] + attribute :messages, Array[VAProfile::Models::Message] - def initialize(response) - @body = response.body - contacts = body.dig('profile', 'health_benefit', 'associated_persons') - &.sort_by { |p| VAProfile::Models::AssociatedPerson::CONTACT_TYPES.index(p['contact_type']) } - messages = body['messages'] - super(response.status, { contacts:, messages: }) - end + def initialize(response) + @body = response.body + contacts = body.dig('profile', 'health_benefit', 'associated_persons') + &.select { |p| valid_contact_types.include?(p['contact_type']) } + &.sort_by { |p| valid_contact_types.index(p['contact_type']) } + messages = body['messages'] + super(response.status, { contacts:, messages: }) + end + + def metadata + { status:, messages: } + end + + private - def metadata - { status:, messages: } + def valid_contact_types + [ + VAProfile::Models::AssociatedPerson::EMERGENCY_CONTACT, + VAProfile::Models::AssociatedPerson::OTHER_EMERGENCY_CONTACT, + VAProfile::Models::AssociatedPerson::PRIMARY_NEXT_OF_KIN, + VAProfile::Models::AssociatedPerson::OTHER_NEXT_OF_KIN + ] + end + end end end end diff --git a/modules/check_in/app/controllers/check_in/v2/patient_check_ins_controller.rb b/modules/check_in/app/controllers/check_in/v2/patient_check_ins_controller.rb index 9a283417b16..582c0f6f63c 100644 --- a/modules/check_in/app/controllers/check_in/v2/patient_check_ins_controller.rb +++ b/modules/check_in/app/controllers/check_in/v2/patient_check_ins_controller.rb @@ -7,7 +7,8 @@ class PatientCheckInsController < CheckIn::ApplicationController after_action :after_logger, only: %i[show create] def show - check_in_session = CheckIn::V2::Session.build(data: { uuid: params[:id], handoff: handoff? }, + check_in_session = CheckIn::V2::Session.build(data: { uuid: params[:id], handoff: handoff?, + facility_type: params[:facilityType] }, jwt: low_auth_token) unless check_in_session.authorized? diff --git a/modules/check_in/app/models/check_in/v2/session.rb b/modules/check_in/app/models/check_in/v2/session.rb index 0678b23c931..a17d7efab75 100644 --- a/modules/check_in/app/models/check_in/v2/session.rb +++ b/modules/check_in/app/models/check_in/v2/session.rb @@ -25,7 +25,7 @@ class Session DOB_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/ LAST_NAME_REGEX = /^.{1,600}$/ - attr_reader :uuid, :dob, :last_name, :settings, :jwt, :check_in_type, :handoff + attr_reader :uuid, :dob, :last_name, :settings, :jwt, :check_in_type, :handoff, :facility_type def_delegators :settings, :redis_session_prefix @@ -46,6 +46,7 @@ def initialize(opts) @last_name = opts.dig(:data, :last_name) @check_in_type = opts.dig(:data, :check_in_type) @handoff = opts.dig(:data, :handoff) + @facility_type = opts.dig(:data, :facility_type) end # diff --git a/modules/check_in/app/services/v2/lorota/service.rb b/modules/check_in/app/services/v2/lorota/service.rb index 6504b0cfcab..cc339b99b74 100644 --- a/modules/check_in/app/services/v2/lorota/service.rb +++ b/modules/check_in/app/services/v2/lorota/service.rb @@ -87,7 +87,7 @@ def check_in_data raw_data = if token.present? - chip_service.refresh_appointments if appointment_identifiers.present? + chip_service.refresh_appointments if refresh_appointments? lorota_client.data(token:) end @@ -101,15 +101,16 @@ def check_in_data patient_check_in.approved end - def appointment_identifiers - Rails.cache.read( + private + + def refresh_appointments? + appointment_identifiers = Rails.cache.read( "check_in_lorota_v2_appointment_identifiers_#{check_in.uuid}", namespace: 'check-in-lorota-v2-cache' ) + appointment_identifiers.present? && !'oh'.casecmp?(check_in.facility_type) end - private - def error_message_handler(e) case Oj.load(e.original_body).fetch('error').strip.downcase when *LOROTA_401_ERROR_MESSAGES diff --git a/modules/check_in/spec/models/check_in/v2/session_spec.rb b/modules/check_in/spec/models/check_in/v2/session_spec.rb index a2ecd3bc8e7..8d53eb70e0e 100644 --- a/modules/check_in/spec/models/check_in/v2/session_spec.rb +++ b/modules/check_in/spec/models/check_in/v2/session_spec.rb @@ -52,6 +52,10 @@ it 'responds to handoff' do expect(subject.build({}).respond_to?(:handoff)).to be(true) end + + it 'responds to facility_type' do + expect(subject.build({}).respond_to?(:facility_type)).to be(true) + end end describe '#valid?' do diff --git a/modules/check_in/spec/services/v2/lorota/service_spec.rb b/modules/check_in/spec/services/v2/lorota/service_spec.rb index 84ec3476305..500086b98f6 100644 --- a/modules/check_in/spec/services/v2/lorota/service_spec.rb +++ b/modules/check_in/spec/services/v2/lorota/service_spec.rb @@ -680,6 +680,10 @@ .and_return(Faraday::Response.new(response_body: appointment_data.to_json, status: 200)) end + it 'returns approved data' do + expect(subject.build(check_in: valid_check_in).check_in_data).to eq(approved_response) + end + context 'when check_in_type is preCheckIn' do let(:opts) { { data: { check_in_type: 'preCheckIn' } } } let(:pre_check_in) { CheckIn::V2::Session.build(opts) } @@ -702,8 +706,30 @@ end end - it 'returns approved data' do - expect(subject.build(check_in: valid_check_in).check_in_data).to eq(approved_response) + context 'when appt identifiers are not present' do + it 'does not call refresh_appts' do + expect_any_instance_of(::V2::Chip::Service).not_to receive(:refresh_appointments) + + expect(subject.build(check_in: valid_check_in).check_in_data).to eq(approved_response) + end + end + + context 'when appt identifiers are present and facility type is OH' do + let(:valid_check_in_oh) { CheckIn::V2::Session.build(opts.deep_merge!({ data: { facility_type: 'oh' } })) } + + before do + Rails.cache.write( + "check_in_lorota_v2_appointment_identifiers_#{id}", + '123', + namespace: 'check-in-lorota-v2-cache' + ) + end + + it 'does not call refresh_appts' do + expect_any_instance_of(::V2::Chip::Service).not_to receive(:refresh_appointments) + + expect(subject.build(check_in: valid_check_in_oh).check_in_data).to eq(approved_response) + end end end end diff --git a/modules/claims_api/app/swagger/claims_api/description/v2.md b/modules/claims_api/app/swagger/claims_api/description/v2.md index cb01ef73d9e..50f6fe94d65 100644 --- a/modules/claims_api/app/swagger/claims_api/description/v2.md +++ b/modules/claims_api/app/swagger/claims_api/description/v2.md @@ -6,14 +6,11 @@ The Benefits Claims API Version 2 lets internal consumers: - Automatically establish an Intent To File (21-0966) in VBMS - Automatically establish a disability compensation claim (21-526EZ) in VBMS - Digitally submit supporting documentation for disability compensation claims -- Retrieve the active Power of Attorney for a Veteran +- Retrieve the active Power of Attorney organization of individual with power of attorney for a Veteran +- Automatically establish a power of attorney appointment in VBMS for an accredited organization (VA Form 21-22). +- Automatically establish a power of attorney appointment in VBMS for an accredited individual (VA Form 21-22a). -Additional functionality will be added over time. - -You should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if:  - -- You are a consumer outside of VA and do not have the necessary VA agreements to use this API, and/or -- You want automatic establishment of power of attorney (21-22 or 21-22a) +You should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if you are a consumer outside of VA and do not have the necessary VA agreements to use this API. ## Technical Overview 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 45d2acf2493..0298380c65d 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 @@ -3,7 +3,7 @@ "info": { "title": "Benefits Claims", "version": "v2", - "description": "## Background\n\nThe Benefits Claims API Version 2 lets internal consumers: \n\n- Retrieve existing claim information, including status, by claim ID\n- Automatically establish an Intent To File (21-0966) in VBMS\n- Automatically establish a disability compensation claim (21-526EZ) in VBMS\n- Digitally submit supporting documentation for disability compensation claims\n- Retrieve the active Power of Attorney for a Veteran\n\nAdditional functionality will be added over time.\n\nYou should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if: \n\n- You are a consumer outside of VA and do not have the necessary VA agreements to use this API, and/or\n- You want automatic establishment of power of attorney (21-22 or 21-22a)\n \n## Technical Overview\n\nThis API accepts a payload of requests and responses with the payload identifying the claim and Veteran. Responses provide the submission’s processing status. Responses also provide a unique ID which can be used with the appropriate GET endpoint to return detailed, end-to-end claims status tracking. \n\nEnd-to-end claims tracking provides the status of claims as they move through the submission process, but does not return whether the claim was approved or denied. \n\n### Claim statuses\n\nClaims are first submitted by this API and then established in Veterans Benefits Management System (VBMS). A 200 response means that the claim was successfully submitted by the API. It does not mean VA has received the claim. Use the appropriate GET endpoint and the ID returned with your submission response to confirm the status of the submission. Statuses are:\n\n- Pending - the claim is successfully submitted for processing\n- Errored - the submission encountered upstream errors\n- Canceled - the claim was identified as a duplicate or another issue caused the claim to be canceled. For duplicate claims, the tracking of the claim's progress happens under a different Claim ID . \n\nOther statuses this API returns align with the [VA.gov](http://va.gov/) [claim status descriptions](https://www.va.gov/resources/what-your-claim-status-means/), which are:\n\n- Claim received\n- Initial review\n- Evidence gathering, review, and decision\n- Preparation for notification\n- Complete\n\n### Finding a Veteran's unique VA ID\n\nThis API uses a unique Veteran identifier to identify the subject of each API request. This Veteran identifier can be retrieved by passing the Veteran’s first name, last name, DOB, and SSN to the ‘/veteran-id’ endpoint. This identifier should then be used as the Veteran ID parameter in request URLs.\n\nNote: though Veteran identifiers are typically static, they may change over time. If a specific Veteran ID suddenly responds with a ‘404 not found’ error, the identifier may have changed. It’s a good idea to periodically check the identifier for each Veteran.\n\n### Authentication and authorization\n\nThe authentication model for the Benefits Claims Version 2 is based on OAuth 2.0 / OpenID Connect and supports the [client credentials grant](https://developer.va.gov/explore/authorization/docs/client-credentials?api=claims).\n\n**Important**: To get production access, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://developer.va.gov/support/contact-us).\n\n### Test data for sandbox environment use\n\nWe use mock [test data in the sandbox environment](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts.md). Sandbox test data and test users for the Benefits Claims API are valid for all versions of the API.\n" + "description": "## Background\n\nThe Benefits Claims API Version 2 lets internal consumers: \n\n- Retrieve existing claim information, including status, by claim ID\n- Automatically establish an Intent To File (21-0966) in VBMS\n- Automatically establish a disability compensation claim (21-526EZ) in VBMS\n- Digitally submit supporting documentation for disability compensation claims\n- Retrieve the active Power of Attorney organization of individual with power of attorney for a Veteran\n- Automatically establish a power of attorney appointment in VBMS for an accredited organization (VA Form 21-22).\n- Automatically establish a power of attorney appointment in VBMS for an accredited individual (VA Form 21-22a).\n\nYou should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if you are a consumer outside of VA and do not have the necessary VA agreements to use this API.\n \n## Technical Overview\n\nThis API accepts a payload of requests and responses with the payload identifying the claim and Veteran. Responses provide the submission’s processing status. Responses also provide a unique ID which can be used with the appropriate GET endpoint to return detailed, end-to-end claims status tracking. \n\nEnd-to-end claims tracking provides the status of claims as they move through the submission process, but does not return whether the claim was approved or denied. \n\n### Claim statuses\n\nClaims are first submitted by this API and then established in Veterans Benefits Management System (VBMS). A 200 response means that the claim was successfully submitted by the API. It does not mean VA has received the claim. Use the appropriate GET endpoint and the ID returned with your submission response to confirm the status of the submission. Statuses are:\n\n- Pending - the claim is successfully submitted for processing\n- Errored - the submission encountered upstream errors\n- Canceled - the claim was identified as a duplicate or another issue caused the claim to be canceled. For duplicate claims, the tracking of the claim's progress happens under a different Claim ID . \n\nOther statuses this API returns align with the [VA.gov](http://va.gov/) [claim status descriptions](https://www.va.gov/resources/what-your-claim-status-means/), which are:\n\n- Claim received\n- Initial review\n- Evidence gathering, review, and decision\n- Preparation for notification\n- Complete\n\n### Finding a Veteran's unique VA ID\n\nThis API uses a unique Veteran identifier to identify the subject of each API request. This Veteran identifier can be retrieved by passing the Veteran’s first name, last name, DOB, and SSN to the ‘/veteran-id’ endpoint. This identifier should then be used as the Veteran ID parameter in request URLs.\n\nNote: though Veteran identifiers are typically static, they may change over time. If a specific Veteran ID suddenly responds with a ‘404 not found’ error, the identifier may have changed. It’s a good idea to periodically check the identifier for each Veteran.\n\n### Authentication and authorization\n\nThe authentication model for the Benefits Claims Version 2 is based on OAuth 2.0 / OpenID Connect and supports the [client credentials grant](https://developer.va.gov/explore/authorization/docs/client-credentials?api=claims).\n\n**Important**: To get production access, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://developer.va.gov/support/contact-us).\n\n### Test data for sandbox environment use\n\nWe use mock [test data in the sandbox environment](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts.md). Sandbox test data and test users for the Benefits Claims API are valid for all versions of the API.\n" }, "tags": [ { @@ -28,7 +28,7 @@ }, { "name": "Power of Attorney", - "description": "Allows authenticated and authorized users to retrieve the active power of attorney for a Veteran\n" + "description": "Allows authenticated and authorized users to automatically establish power of attorney appointments to an organization or an individual. Organizations and individuals must be VA accredited representatives.\n" } ], "components": { @@ -5720,7 +5720,7 @@ "application/json": { "example": { "data": { - "id": "bb700978-5b4c-435d-b8ec-d16914ed3174", + "id": "cf532e4e-e89e-4f9c-aebd-ce8361336a41", "type": "forms/526", "attributes": { "veteran": { @@ -7804,8 +7804,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-03-14", - "expirationDate": "2025-03-14", + "creationDate": "2024-03-26", + "expirationDate": "2025-03-26", "type": "compensation", "status": "active" } @@ -8524,7 +8524,7 @@ "status": "422", "detail": "Could not retrieve Power of Attorney due to multiple representatives with code: A1Q", "source": { - "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb:111:in `representative'" + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb:112:in `representative'" } } ] @@ -8623,7 +8623,7 @@ "application/json": { "example": { "data": { - "id": "3d2234a1-0151-4ddc-921f-7e3575089d6e", + "id": "29b16b36-3108-411f-9f5f-2c1c2e147ea3", "type": "individual", "attributes": { "code": "083", @@ -9323,7 +9323,7 @@ "application/json": { "example": { "data": { - "id": "45bf00c5-36b3-4157-9620-6a89d5bd37ae", + "id": "a7114d11-8ffd-4545-ad99-d70e74991e11", "type": "organization", "attributes": { "code": "083", @@ -11261,11 +11261,11 @@ "application/json": { "example": { "data": { - "id": "d3048cf3-3869-4c31-a873-996e4959b13a", + "id": "7b0c58e1-4bf7-413c-a3ab-c8f6b95208b0", "type": "claimsApiPowerOfAttorneys", "attributes": { "status": "submitted", - "dateRequestAccepted": "2024-03-14", + "dateRequestAccepted": "2024-03-26", "representative": { "serviceOrganization": { "poaCode": "074" diff --git a/modules/claims_api/app/swagger/claims_api/v2/production/swagger.json b/modules/claims_api/app/swagger/claims_api/v2/production/swagger.json index aeeecb629ae..43450b6625d 100644 --- a/modules/claims_api/app/swagger/claims_api/v2/production/swagger.json +++ b/modules/claims_api/app/swagger/claims_api/v2/production/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "Benefits Claims", "version": "v2", - "description": "## Background\n\nThe Benefits Claims API Version 2 lets internal consumers: \n\n- Retrieve existing claim information, including status, by claim ID\n- Automatically establish an Intent To File (21-0966) in VBMS\n- Automatically establish a disability compensation claim (21-526EZ) in VBMS\n- Digitally submit supporting documentation for disability compensation claims\n- Retrieve the active Power of Attorney for a Veteran\n\nAdditional functionality will be added over time.\n\nYou should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if: \n\n- You are a consumer outside of VA and do not have the necessary VA agreements to use this API, and/or\n- You want automatic establishment of power of attorney (21-22 or 21-22a)\n \n## Technical Overview\n\nThis API accepts a payload of requests and responses with the payload identifying the claim and Veteran. Responses provide the submission’s processing status. Responses also provide a unique ID which can be used with the appropriate GET endpoint to return detailed, end-to-end claims status tracking. \n\nEnd-to-end claims tracking provides the status of claims as they move through the submission process, but does not return whether the claim was approved or denied. \n\n### Claim statuses\n\nClaims are first submitted by this API and then established in Veterans Benefits Management System (VBMS). A 200 response means that the claim was successfully submitted by the API. It does not mean VA has received the claim. Use the appropriate GET endpoint and the ID returned with your submission response to confirm the status of the submission. Statuses are:\n\n- Pending - the claim is successfully submitted for processing\n- Errored - the submission encountered upstream errors\n- Canceled - the claim was identified as a duplicate or another issue caused the claim to be canceled. For duplicate claims, the tracking of the claim's progress happens under a different Claim ID . \n\nOther statuses this API returns align with the [VA.gov](http://va.gov/) [claim status descriptions](https://www.va.gov/resources/what-your-claim-status-means/), which are:\n\n- Claim received\n- Initial review\n- Evidence gathering, review, and decision\n- Preparation for notification\n- Complete\n\n### Finding a Veteran's unique VA ID\n\nThis API uses a unique Veteran identifier to identify the subject of each API request. This Veteran identifier can be retrieved by passing the Veteran’s first name, last name, DOB, and SSN to the ‘/veteran-id’ endpoint. This identifier should then be used as the Veteran ID parameter in request URLs.\n\nNote: though Veteran identifiers are typically static, they may change over time. If a specific Veteran ID suddenly responds with a ‘404 not found’ error, the identifier may have changed. It’s a good idea to periodically check the identifier for each Veteran.\n\n### Authentication and authorization\n\nThe authentication model for the Benefits Claims Version 2 is based on OAuth 2.0 / OpenID Connect and supports the [client credentials grant](https://developer.va.gov/explore/authorization/docs/client-credentials?api=claims).\n\n**Important**: To get production access, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://developer.va.gov/support/contact-us).\n\n### Test data for sandbox environment use\n\nWe use mock [test data in the sandbox environment](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts.md). Sandbox test data and test users for the Benefits Claims API are valid for all versions of the API.\n" + "description": "## Background\n\nThe Benefits Claims API Version 2 lets internal consumers: \n\n- Retrieve existing claim information, including status, by claim ID\n- Automatically establish an Intent To File (21-0966) in VBMS\n- Automatically establish a disability compensation claim (21-526EZ) in VBMS\n- Digitally submit supporting documentation for disability compensation claims\n- Retrieve the active Power of Attorney organization of individual with power of attorney for a Veteran\n- Automatically establish a power of attorney appointment in VBMS for an accredited organization (VA Form 21-22).\n- Automatically establish a power of attorney appointment in VBMS for an accredited individual (VA Form 21-22a).\n\nYou should use the [Benefits Claims API Version 1](https://developer.va.gov/explore/benefits/docs/claims?version=current) if you are a consumer outside of VA and do not have the necessary VA agreements to use this API.\n \n## Technical Overview\n\nThis API accepts a payload of requests and responses with the payload identifying the claim and Veteran. Responses provide the submission’s processing status. Responses also provide a unique ID which can be used with the appropriate GET endpoint to return detailed, end-to-end claims status tracking. \n\nEnd-to-end claims tracking provides the status of claims as they move through the submission process, but does not return whether the claim was approved or denied. \n\n### Claim statuses\n\nClaims are first submitted by this API and then established in Veterans Benefits Management System (VBMS). A 200 response means that the claim was successfully submitted by the API. It does not mean VA has received the claim. Use the appropriate GET endpoint and the ID returned with your submission response to confirm the status of the submission. Statuses are:\n\n- Pending - the claim is successfully submitted for processing\n- Errored - the submission encountered upstream errors\n- Canceled - the claim was identified as a duplicate or another issue caused the claim to be canceled. For duplicate claims, the tracking of the claim's progress happens under a different Claim ID . \n\nOther statuses this API returns align with the [VA.gov](http://va.gov/) [claim status descriptions](https://www.va.gov/resources/what-your-claim-status-means/), which are:\n\n- Claim received\n- Initial review\n- Evidence gathering, review, and decision\n- Preparation for notification\n- Complete\n\n### Finding a Veteran's unique VA ID\n\nThis API uses a unique Veteran identifier to identify the subject of each API request. This Veteran identifier can be retrieved by passing the Veteran’s first name, last name, DOB, and SSN to the ‘/veteran-id’ endpoint. This identifier should then be used as the Veteran ID parameter in request URLs.\n\nNote: though Veteran identifiers are typically static, they may change over time. If a specific Veteran ID suddenly responds with a ‘404 not found’ error, the identifier may have changed. It’s a good idea to periodically check the identifier for each Veteran.\n\n### Authentication and authorization\n\nThe authentication model for the Benefits Claims Version 2 is based on OAuth 2.0 / OpenID Connect and supports the [client credentials grant](https://developer.va.gov/explore/authorization/docs/client-credentials?api=claims).\n\n**Important**: To get production access, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://developer.va.gov/support/contact-us).\n\n### Test data for sandbox environment use\n\nWe use mock [test data in the sandbox environment](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts.md). Sandbox test data and test users for the Benefits Claims API are valid for all versions of the API.\n" }, "tags": [ { @@ -28,7 +28,7 @@ }, { "name": "Power of Attorney", - "description": "Allows authenticated and authorized users to retrieve the active power of attorney for a Veteran\n" + "description": "Allows authenticated and authorized users to automatically establish power of attorney appointments to an organization or an individual. Organizations and individuals must be VA accredited representatives.\n" } ], "components": { @@ -5720,7 +5720,7 @@ "application/json": { "example": { "data": { - "id": "450bd9ed-ec42-414a-a338-d79c766f7c8e", + "id": "8b3c6607-078b-419b-8549-726da40193df", "type": "forms/526", "attributes": { "veteran": { @@ -5871,7 +5871,7 @@ "status": "404", "detail": "Resource not found", "source": { - "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb:66:in `attachments'" + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb:70:in `attachments'" } } ] @@ -7804,8 +7804,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-02-12", - "expirationDate": "2025-02-12", + "creationDate": "2024-03-26", + "expirationDate": "2025-03-26", "type": "compensation", "status": "active" } @@ -8524,7 +8524,2910 @@ "status": "422", "detail": "Could not retrieve Power of Attorney due to multiple representatives with code: A1Q", "source": { - "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney_controller.rb:108:in `representative'" + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb:112:in `representative'" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/veterans/{veteranId}/2122a": { + "post": { + "summary": "Appoint an individual Power of Attorney for a Veteran.", + "tags": [ + "Power of Attorney" + ], + "operationId": "post2122a", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "parameters": [ + { + "name": "veteranId", + "in": "path", + "required": true, + "example": "1012667145V762142", + "description": "ID of Veteran", + "schema": { + "type": "string" + } + } + ], + "description": "Updates current Power of Attorney for Veteran.", + "responses": { + "202": { + "description": "Valid request response", + "content": { + "application/json": { + "example": { + "data": { + "id": "9a9b6db5-abfc-45f3-ab60-785e3fb052ed", + "type": "individual", + "attributes": { + "code": "083", + "name": "Firstname Lastname", + "phoneNumber": "555-555-5555" + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "attributes" + ], + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "description": "code for Power of attorney" + }, + "phoneNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Not authorized", + "status": "401", + "detail": "Not authorized" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Unprocessable entity", + "detail": "The property /representative did not contain the required key poaCode", + "status": "422", + "source": { + "pointer": "data/attributes/representative" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property / did not contain the required key veteran", + "status": "422", + "source": { + "pointer": "data/attributes/" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Resource not found", + "status": "404", + "detail": "Could not find an Accredited Representative with code: 083", + "source": { + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/individual_controller.rb:35:in `validate_individual_poa_code!'" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "attributes", + null + ], + "properties": { + "attributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Form 2122a Schema", + "type": "object", + "additionalProperties": false, + "required": [ + "veteran", + "representative" + ], + "properties": { + "veteran": { + "type": "object", + "additionalProperties": false, + "required": [ + "address" + ], + "properties": { + "serviceNumber": { + "description": "The Veteran's Service Number", + "type": "string", + "maxLength": 9 + }, + "serviceBranch": { + "description": "Service Branch for the veteran.", + "type": "string", + "enum": [ + "AIR_FORCE", + "ARMY", + "COAST_GUARD", + "MARINE_CORPS", + "NAVY", + "SPACE_FORCE", + "OTHER" + ], + "example": "ARMY" + }, + "serviceBranchOther": { + "description": "For a 'service branch' of value 'other', please provide the service branch name.", + "type": "string", + "maxLength": 27, + "example": "Air National Guard" + }, + "address": { + "type": "object", + "additionalProperties": false, + "required": [ + "addressLine1", + "city", + "stateCode", + "country", + "zipCode" + ], + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the veteran.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran@example.com" + } + } + }, + "claimant": { + "type": "object", + "additionalProperties": false, + "properties": { + "claimantId": { + "type": "string", + "example": "123456789", + "description": "Id of the claimant." + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name. Required if claimant information provided.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address. Required if claimant information provided.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address. Required if claimant information provided.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address. Required if claimant information provided.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address. Required if claimant information provided.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the claimant.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "claimant@example.com" + }, + "relationship": { + "description": "Relationship of claimant to the veteran. Required if claimant information provided.", + "type": "string", + "example": "Spouse" + } + } + }, + "representative": { + "description": "Details of the individual representative representing the veteran.", + "type": "object", + "additionalProperties": false, + "required": [ + "poaCode", + "firstName", + "lastName", + "type" + ], + "properties": { + "poaCode": { + "description": "The POA code of the representative.", + "type": "string", + "example": "A1Q" + }, + "firstName": { + "description": "First Name of the representative.", + "type": "string", + "example": "John" + }, + "lastName": { + "description": "Last Name of the representative", + "type": "string", + "example": "Doe" + }, + "type": { + "description": "Type of individual representative", + "type": "string", + "enum": [ + "ATTORNEY", + "AGENT" + ], + "example": "ATTORNEY" + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "organizationName": { + "description": "Name of the service organization.", + "type": "string", + "example": "I help vets LLC." + } + } + }, + "recordConsent": { + "description": "AUTHORIZATION FOR REPRESENTATIVE'S ACCESS TO RECORDS PROTECTED BY SECTION 7332, TITLE 38, U.S.C.", + "type": "boolean" + }, + "consentLimits": { + "description": "Consent in Item 19 for the disclosure of records relating to treatment for drug abuse, alcoholism or alcohol abuse, infection with the human immunodeficiency virus (HIV), or sickle cell anemia is limited as follows.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "DRUG_ABUSE", + "ALCOHOLISM", + "HIV", + "SICKLE_CELL" + ] + }, + "example": "DRUG ABUSE" + }, + "consentAddressChange": { + "description": "AUTHORIZATION FOR REPRESENTATIVE TO ACT ON CLAIMANT'S BEHALF TO CHANGE CLAIMANT'S ADDRESS.", + "type": "boolean" + }, + "conditionsOfAppointment": { + "description": "If the individual named in Item 15A is an accredited agent or attorney, the scope of representation provided before VA may be limited by the agent or attorney as indicated below in Item 23", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "example": { + "data": { + "attributes": { + "veteran": { + "address": { + "addressLine1": "123", + "addressLine2": "2a", + "city": "city", + "country": "US", + "stateCode": "OR", + "zipCode": "12345", + "zipCodeSuffix": "6789" + } + }, + "representative": { + "poaCode": "083", + "firstName": "my", + "lastName": "name", + "type": "ATTORNEY", + "address": { + "addressLine1": "123", + "addressLine2": "2a", + "city": "city", + "country": "US", + "stateCode": "OR", + "zipCode": "12345", + "zipCodeSuffix": "6789" + } + } + } + } + } + } + } + }, + "required": true + } + } + }, + "/veterans/{veteranId}/2122": { + "post": { + "summary": "Appoint an organization Power of Attorney for a Veteran.", + "description": "Updates current Power of Attorney for Veteran.", + "tags": [ + "Power of Attorney" + ], + "operationId": "post2122", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "parameters": [ + { + "name": "veteranId", + "in": "path", + "required": true, + "example": "1012667145V762142", + "description": "ID of Veteran", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Valid request response", + "content": { + "application/json": { + "example": { + "data": { + "id": "618d9ba4-44cf-490a-bd56-8012b59b30e7", + "type": "organization", + "attributes": { + "code": "083", + "name": "083 - DISABLED AMERICAN VETERANS", + "phoneNumber": "555-555-5555" + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "attributes" + ], + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "description": "code for Power of attorney" + }, + "phoneNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Not authorized", + "status": "401", + "detail": "Not authorized" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization did not contain the required key poaCode", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property / did not contain the required key veteran", + "status": "422", + "source": { + "pointer": "data/attributes/" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Resource not found", + "status": "404", + "detail": "Could not find an Organization with code: 083", + "source": { + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/organization_controller.rb:35:in `validate_org_poa_code!'" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "attributes", + null + ], + "properties": { + "attributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Form 2122 Schema", + "type": "object", + "additionalProperties": false, + "required": [ + "veteran", + "serviceOrganization" + ], + "properties": { + "veteran": { + "type": "object", + "additionalProperties": false, + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false, + "required": [ + "addressLine1", + "city", + "country", + "stateCode", + "zipCode" + ], + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the veteran.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran@example.com" + }, + "serviceNumber": { + "description": "Service number for the veteran.", + "type": "string", + "pattern": "^\\d{9}?$", + "example": "123456789" + } + } + }, + "claimant": { + "type": "object", + "additionalProperties": false, + "properties": { + "claimantId": { + "type": "string", + "example": "123456789", + "description": "Id of the claimant." + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name. Required if claimant information provided.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address. Required if claimant information provided.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address. Required if claimant information provided.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address. Required if claimant information provided.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address. Required if claimant information provided.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the claimant.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "claimant@example.com" + }, + "relationship": { + "description": "Relationship of claimant to the veteran. Required if claimant information provided.", + "type": "string", + "example": "Spouse" + } + } + }, + "serviceOrganization": { + "description": "Details of the Service Organization representing the veteran.", + "type": "object", + "additionalProperties": false, + "required": [ + "poaCode" + ], + "properties": { + "poaCode": { + "description": "The POA code of the organization.", + "type": "string", + "example": "A1Q" + }, + "organizationName": { + "description": "Name of the service organization.", + "type": "string", + "example": "I help vets LLC." + }, + "firstName": { + "description": "First Name of the representative.", + "type": "string", + "example": "John" + }, + "lastName": { + "description": "Last Name of the representative", + "type": "string", + "example": "Doe" + }, + "jobTitle": { + "description": "Job title of the representative.", + "type": "string", + "example": "Veteran Service representative" + }, + "email": { + "description": "Email address of the service organization or representative.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran_representative@example.com" + }, + "appointmentDate": { + "description": "Date of appointment with Veteran.", + "type": "string", + "pattern": "^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$" + } + } + }, + "recordConsent": { + "description": "AUTHORIZATION FOR REPRESENTATIVE'S ACCESS TO RECORDS PROTECTED BY SECTION 7332, TITLE 38, U.S.C.", + "type": "boolean" + }, + "consentLimits": { + "description": "Consent in Item 19 for the disclosure of records relating to treatment for drug abuse, alcoholism or alcohol abuse, infection with the human immunodeficiency virus (HIV), or sickle cell anemia is limited as follows.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "DRUG_ABUSE", + "ALCOHOLISM", + "HIV", + "SICKLE_CELL" + ] + }, + "example": "DRUG_ABUSE" + }, + "consentAddressChange": { + "description": "AUTHORIZATION FOR REPRESENTATIVE TO ACT ON CLAIMANT'S BEHALF TO CHANGE CLAIMANT'S ADDRESS.", + "type": "boolean" + } + } + } + } + } + }, + "example": { + "data": { + "attributes": { + "veteran": { + "address": { + "addressLine1": "123", + "city": "city", + "stateCode": "OR", + "country": "US", + "zipCode": "12345" + } + }, + "serviceOrganization": { + "poaCode": "083" + } + } + } + } + } + } + }, + "required": true + } + } + }, + "/veterans/{veteranId}/2122a/validate": { + "post": { + "summary": "Validates a 2122a form submission.", + "tags": [ + "Power of Attorney" + ], + "operationId": "post2122aValidate", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "parameters": [ + { + "name": "veteranId", + "in": "path", + "required": true, + "example": "1012667145V762142", + "description": "ID of Veteran", + "schema": { + "type": "string" + } + } + ], + "description": "Validates a request appointing an individual as Power of Attorney (21-22a).\n", + "responses": { + "200": { + "description": "Valid request response", + "content": { + "application/json": { + "example": { + "data": { + "type": "form/21-22a/validation", + "attributes": { + "status": "valid" + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "attributes" + ], + "properties": { + "type": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "Says if submission of 21-22a would work with the given parameters", + "enum": [ + "valid" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Not authorized", + "status": "401", + "detail": "Not authorized" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Unprocessable entity", + "detail": "The property /representative did not contain the required key poaCode", + "status": "422", + "source": { + "pointer": "data/attributes/representative" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property / did not contain the required key veteran", + "status": "422", + "source": { + "pointer": "data/attributes/" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Resource not found", + "status": "404", + "detail": "Could not find an Accredited Representative with code: 083", + "source": { + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/individual_controller.rb:35:in `validate_individual_poa_code!'" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "attributes", + null + ], + "properties": { + "attributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Form 2122a Schema", + "type": "object", + "additionalProperties": false, + "required": [ + "veteran", + "representative" + ], + "properties": { + "veteran": { + "type": "object", + "additionalProperties": false, + "required": [ + "address" + ], + "properties": { + "serviceNumber": { + "description": "The Veteran's Service Number", + "type": "string", + "maxLength": 9 + }, + "serviceBranch": { + "description": "Service Branch for the veteran.", + "type": "string", + "enum": [ + "AIR_FORCE", + "ARMY", + "COAST_GUARD", + "MARINE_CORPS", + "NAVY", + "SPACE_FORCE", + "OTHER" + ], + "example": "ARMY" + }, + "serviceBranchOther": { + "description": "For a 'service branch' of value 'other', please provide the service branch name.", + "type": "string", + "maxLength": 27, + "example": "Air National Guard" + }, + "address": { + "type": "object", + "additionalProperties": false, + "required": [ + "addressLine1", + "city", + "stateCode", + "country", + "zipCode" + ], + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the veteran.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran@example.com" + } + } + }, + "claimant": { + "type": "object", + "additionalProperties": false, + "properties": { + "claimantId": { + "type": "string", + "example": "123456789", + "description": "Id of the claimant." + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name. Required if claimant information provided.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address. Required if claimant information provided.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address. Required if claimant information provided.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address. Required if claimant information provided.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address. Required if claimant information provided.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the claimant.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "claimant@example.com" + }, + "relationship": { + "description": "Relationship of claimant to the veteran. Required if claimant information provided.", + "type": "string", + "example": "Spouse" + } + } + }, + "representative": { + "description": "Details of the individual representative representing the veteran.", + "type": "object", + "additionalProperties": false, + "required": [ + "poaCode", + "firstName", + "lastName", + "type" + ], + "properties": { + "poaCode": { + "description": "The POA code of the representative.", + "type": "string", + "example": "A1Q" + }, + "firstName": { + "description": "First Name of the representative.", + "type": "string", + "example": "John" + }, + "lastName": { + "description": "Last Name of the representative", + "type": "string", + "example": "Doe" + }, + "type": { + "description": "Type of individual representative", + "type": "string", + "enum": [ + "ATTORNEY", + "AGENT" + ], + "example": "ATTORNEY" + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "organizationName": { + "description": "Name of the service organization.", + "type": "string", + "example": "I help vets LLC." + } + } + }, + "recordConsent": { + "description": "AUTHORIZATION FOR REPRESENTATIVE'S ACCESS TO RECORDS PROTECTED BY SECTION 7332, TITLE 38, U.S.C.", + "type": "boolean" + }, + "consentLimits": { + "description": "Consent in Item 19 for the disclosure of records relating to treatment for drug abuse, alcoholism or alcohol abuse, infection with the human immunodeficiency virus (HIV), or sickle cell anemia is limited as follows.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "DRUG_ABUSE", + "ALCOHOLISM", + "HIV", + "SICKLE_CELL" + ] + }, + "example": "DRUG ABUSE" + }, + "consentAddressChange": { + "description": "AUTHORIZATION FOR REPRESENTATIVE TO ACT ON CLAIMANT'S BEHALF TO CHANGE CLAIMANT'S ADDRESS.", + "type": "boolean" + }, + "conditionsOfAppointment": { + "description": "If the individual named in Item 15A is an accredited agent or attorney, the scope of representation provided before VA may be limited by the agent or attorney as indicated below in Item 23", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "example": { + "data": { + "attributes": { + "veteran": { + "address": { + "addressLine1": "123", + "addressLine2": "2a", + "city": "city", + "country": "US", + "stateCode": "OR", + "zipCode": "12345", + "zipCodeSuffix": "6789" + } + }, + "representative": { + "poaCode": "083", + "firstName": "my", + "lastName": "name", + "type": "ATTORNEY", + "address": { + "addressLine1": "123", + "addressLine2": "2a", + "city": "city", + "country": "US", + "stateCode": "OR", + "zipCode": "12345", + "zipCodeSuffix": "6789" + } + } + } + } + } + } + } + }, + "required": true + } + } + }, + "/veterans/{veteranId}/2122/validate": { + "post": { + "summary": "Validates a 2122 form submission.", + "tags": [ + "Power of Attorney" + ], + "operationId": "post2122Validate", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "parameters": [ + { + "name": "veteranId", + "in": "path", + "required": true, + "example": "1012667145V762142", + "description": "ID of Veteran", + "schema": { + "type": "string" + } + } + ], + "description": "Validates a request appointing an organization as Power of Attorney (21-22).\n", + "responses": { + "200": { + "description": "Valid request response", + "content": { + "application/json": { + "example": { + "data": { + "type": "form/21-22/validation", + "attributes": { + "status": "valid" + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "attributes" + ], + "properties": { + "type": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "Says if submission of 21-22 would work with the given parameters", + "enum": [ + "valid" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Not authorized", + "status": "401", + "detail": "Not authorized" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization did not contain the required key poaCode", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property / did not contain the required key veteran", + "status": "422", + "source": { + "pointer": "data/attributes/" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Resource not found", + "status": "404", + "detail": "Could not find an Organization with code: 083", + "source": { + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/organization_controller.rb:35:in `validate_org_poa_code!'" + } + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "attributes", + null + ], + "properties": { + "attributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Form 2122 Schema", + "type": "object", + "additionalProperties": false, + "required": [ + "veteran", + "serviceOrganization" + ], + "properties": { + "veteran": { + "type": "object", + "additionalProperties": false, + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false, + "required": [ + "addressLine1", + "city", + "country", + "stateCode", + "zipCode" + ], + "properties": { + "addressLine1": { + "description": "Street address with number and name.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the veteran.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran@example.com" + }, + "serviceNumber": { + "description": "Service number for the veteran.", + "type": "string", + "pattern": "^\\d{9}?$", + "example": "123456789" + } + } + }, + "claimant": { + "type": "object", + "additionalProperties": false, + "properties": { + "claimantId": { + "type": "string", + "example": "123456789", + "description": "Id of the claimant." + }, + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "addressLine1": { + "description": "Street address with number and name. Required if claimant information provided.", + "type": "string", + "pattern": "^([-a-zA-Z0-9'.,&#]([-a-zA-Z0-9'.,&# ])?)+$", + "maxLength": 30 + }, + "addressLine2": { + "type": "string", + "maxLength": 5 + }, + "city": { + "description": "City for the address. Required if claimant information provided.", + "type": "string", + "example": "Portland", + "maxLength": 18 + }, + "stateCode": { + "description": "State for the address. Required if claimant information provided.", + "type": "string", + "pattern": "^[a-z,A-Z]{2}$", + "example": "OR" + }, + "country": { + "description": "Country of the address. Required if claimant information provided.", + "type": "string", + "example": "USA" + }, + "zipCode": { + "description": "Zipcode (First 5 digits) of the address. Required if claimant information provided.", + "type": "string", + "pattern": "^\\d{5}?$", + "example": "12345" + }, + "zipCodeSuffix": { + "description": "Zipcode (Last 4 digits) of the address.", + "type": "string", + "pattern": "^\\d{4}?$", + "example": "6789" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "phone": { + "$comment": "the phone fields must not exceed 20 chars, when concatenated", + "type": "object", + "additionalProperties": false, + "required": [ + "areaCode", + "phoneNumber" + ], + "properties": { + "countryCode": { + "type": "string", + "pattern": "^[0-9]+$" + }, + "areaCode": { + "description": "Area code of the phone number.", + "type": "string", + "pattern": "^[2-9][0-9]{2}$", + "example": "555" + }, + "phoneNumber": { + "description": "Phone number.", + "type": "string", + "pattern": "^[0-9]{1,14}$", + "example": "555-5555" + }, + "phoneNumberExt": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{1,10}$" + } + } + }, + "email": { + "description": "Email address of the claimant.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "claimant@example.com" + }, + "relationship": { + "description": "Relationship of claimant to the veteran. Required if claimant information provided.", + "type": "string", + "example": "Spouse" + } + } + }, + "serviceOrganization": { + "description": "Details of the Service Organization representing the veteran.", + "type": "object", + "additionalProperties": false, + "required": [ + "poaCode" + ], + "properties": { + "poaCode": { + "description": "The POA code of the organization.", + "type": "string", + "example": "A1Q" + }, + "organizationName": { + "description": "Name of the service organization.", + "type": "string", + "example": "I help vets LLC." + }, + "firstName": { + "description": "First Name of the representative.", + "type": "string", + "example": "John" + }, + "lastName": { + "description": "Last Name of the representative", + "type": "string", + "example": "Doe" + }, + "jobTitle": { + "description": "Job title of the representative.", + "type": "string", + "example": "Veteran Service representative" + }, + "email": { + "description": "Email address of the service organization or representative.", + "type": "string", + "pattern": ".@.", + "maxLength": 61, + "example": "veteran_representative@example.com" + }, + "appointmentDate": { + "description": "Date of appointment with Veteran.", + "type": "string", + "pattern": "^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$" + } + } + }, + "recordConsent": { + "description": "AUTHORIZATION FOR REPRESENTATIVE'S ACCESS TO RECORDS PROTECTED BY SECTION 7332, TITLE 38, U.S.C.", + "type": "boolean" + }, + "consentLimits": { + "description": "Consent in Item 19 for the disclosure of records relating to treatment for drug abuse, alcoholism or alcohol abuse, infection with the human immunodeficiency virus (HIV), or sickle cell anemia is limited as follows.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "DRUG_ABUSE", + "ALCOHOLISM", + "HIV", + "SICKLE_CELL" + ] + }, + "example": "DRUG_ABUSE" + }, + "consentAddressChange": { + "description": "AUTHORIZATION FOR REPRESENTATIVE TO ACT ON CLAIMANT'S BEHALF TO CHANGE CLAIMANT'S ADDRESS.", + "type": "boolean" + } + } + } + } + } + }, + "example": { + "data": { + "attributes": { + "veteran": { + "address": { + "addressLine1": "123", + "city": "city", + "stateCode": "OR", + "country": "US", + "zipCode": "12345" + } + }, + "serviceOrganization": { + "poaCode": "083" + } + } + } + } + } + } + }, + "required": true + } + } + }, + "/veterans/{veteranId}/power-of-attorney/{id}": { + "get": { + "summary": "Checks status of Power of Attorney appointment form submission", + "description": "Gets the Power of Attorney appointment request status (21-22/21-22a)", + "tags": [ + "Power of Attorney" + ], + "operationId": "getPowerOfAttorneyStatus", + "security": [ + { + "productionOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "sandboxOauth": [ + "system/claim.read", + "system/claim.write" + ] + }, + { + "bearer_token": [ + + ] + } + ], + "parameters": [ + { + "name": "veteranId", + "in": "path", + "required": true, + "example": "1012667145V762142", + "description": "ID of Veteran", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "example": "12e13134-7229-4e44-90ae-bcea2a4525fa", + "description": "The ID of the 21-22 submission", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Valid request response", + "content": { + "application/json": { + "example": { + "data": { + "id": "5d78a9f2-fffb-4867-a621-6ddee5bd5e58", + "type": "claimsApiPowerOfAttorneys", + "attributes": { + "status": "submitted", + "dateRequestAccepted": "2024-03-26", + "representative": { + "serviceOrganization": { + "poaCode": "074" + } + }, + "previousPoa": null + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "attributes" + ], + "properties": { + "id": { + "type": "string", + "description": "Power of Attorney Submission UUID" + }, + "type": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "dateRequestAccepted", + "representative" + ], + "properties": { + "status": { + "type": "string", + "description": "Says if the power of attorney is pending, submitted, updated or errored", + "enum": [ + "pending", + "submitted", + "updated", + "errored" + ] + }, + "dateRequestAccepted": { + "type": "string", + "description": "Date request was first accepted", + "format": "date" + }, + "representative": { + "type": "object", + "additionalProperties": false, + "required": [ + "serviceOrganization" + ], + "properties": { + "serviceOrganization": { + "type": "object", + "additionalProperties": true, + "required": [ + "poaCode" + ], + "properties": { + "poa_code": { + "type": "string", + "description": "Power of Attorney Code submitted for Veteran" + } + } + } + } + }, + "previousPoa": { + "type": "string", + "nullable": true, + "description": "Current or Previous Power of Attorney Code submitted for Veteran" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Not authorized", + "status": "401", + "detail": "Not authorized" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "status": { + "type": "string", + "description": "HTTP error status code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Resource not found", + "status": "404", + "detail": "Could not find Power of Attorney with id: -1", + "source": { + "pointer": "/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb:32:in `status'" } } ] diff --git a/modules/claims_api/lib/bgs_service/local_bgs.rb b/modules/claims_api/lib/bgs_service/local_bgs.rb index 9e99d25c9cb..a86683900b3 100644 --- a/modules/claims_api/lib/bgs_service/local_bgs.rb +++ b/modules/claims_api/lib/bgs_service/local_bgs.rb @@ -33,8 +33,11 @@ def self.breakers_service url = Settings.bgs.url path = URI.parse(url).path host = URI.parse(url).host + port = URI.parse(url).port matcher = proc do |request_env| - request_env.url.host == host && request_env.url.path =~ /^#{path}/ + request_env.url.host == host && + request_env.url.port == port && + request_env.url.path =~ /^#{path}/ end Breakers::Service.new( diff --git a/modules/claims_api/spec/fixtures/test_client.p12 b/modules/claims_api/spec/fixtures/test_client.p12 index a253a22f3ed..48fde3d8efa 100644 Binary files a/modules/claims_api/spec/fixtures/test_client.p12 and b/modules/claims_api/spec/fixtures/test_client.p12 differ diff --git a/modules/claims_api/spec/requests/v2/veterans/rswag_power_of_attorney_spec.rb b/modules/claims_api/spec/requests/v2/veterans/rswag_power_of_attorney_spec.rb index fb6eebe5285..92075a1c2fb 100644 --- a/modules/claims_api/spec/requests/v2/veterans/rswag_power_of_attorney_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/rswag_power_of_attorney_spec.rb @@ -143,7 +143,7 @@ end end - path '/veterans/{veteranId}/2122a', production: false do + path '/veterans/{veteranId}/2122a' do post 'Appoint an individual Power of Attorney for a Veteran.' do tags 'Power of Attorney' operationId 'post2122a' @@ -312,7 +312,7 @@ end end - path '/veterans/{veteranId}/2122', production: false do + path '/veterans/{veteranId}/2122' do post 'Appoint an organization Power of Attorney for a Veteran.' do description 'Updates current Power of Attorney for Veteran.' tags 'Power of Attorney' @@ -468,7 +468,7 @@ end end - path '/veterans/{veteranId}/2122a/validate', production: false do + path '/veterans/{veteranId}/2122a/validate' do post 'Validates a 2122a form submission.' do tags 'Power of Attorney' operationId 'post2122aValidate' @@ -638,7 +638,7 @@ end end - path '/veterans/{veteranId}/2122/validate', production: false do + path '/veterans/{veteranId}/2122/validate' do post 'Validates a 2122 form submission.' do tags 'Power of Attorney' operationId 'post2122Validate' @@ -796,7 +796,7 @@ end end - path '/veterans/{veteranId}/power-of-attorney/{id}', production: false do + path '/veterans/{veteranId}/power-of-attorney/{id}' do get 'Checks status of Power of Attorney appointment form submission' do description 'Gets the Power of Attorney appointment request status (21-22/21-22a)' tags 'Power of Attorney' diff --git a/modules/claims_api/spec/support/rswag_config.rb b/modules/claims_api/spec/support/rswag_config.rb index 19474e1856e..264da3585fb 100644 --- a/modules/claims_api/spec/support/rswag_config.rb +++ b/modules/claims_api/spec/support/rswag_config.rb @@ -127,7 +127,7 @@ def config # rubocop:disable Metrics/MethodLength { name: 'Power of Attorney', description: <<~VERBIAGE - Allows authenticated and authorized users to retrieve the active power of attorney for a Veteran + Allows authenticated and authorized users to automatically establish power of attorney appointments to an organization or an individual. Organizations and individuals must be VA accredited representatives. VERBIAGE } ], diff --git a/modules/simple_forms_api/app/services/simple_forms_api/pdf_stamper.rb b/modules/simple_forms_api/app/services/simple_forms_api/pdf_stamper.rb index 26999cb00c3..a0f8d61bcb7 100644 --- a/modules/simple_forms_api/app/services/simple_forms_api/pdf_stamper.rb +++ b/modules/simple_forms_api/app/services/simple_forms_api/pdf_stamper.rb @@ -26,7 +26,7 @@ def self.stamp_pdf(stamped_template_path, form, current_loa) end stamp_text = SUBMISSION_TEXT + current_time desired_stamps = [[10, 10, stamp_text]] - stamp(desired_stamps, stamped_template_path, auth_text, text_only: false) + verify(stamped_template_path) { stamp(desired_stamps, stamped_template_path, auth_text, text_only: false) } stamp_submission_date(stamped_template_path, form.submission_date_config) end @@ -34,7 +34,7 @@ def self.stamp_pdf(stamped_template_path, form, current_loa) def self.stamp107959f1(stamped_template_path, form) desired_stamps = [[26, 82.5, form.data['statement_of_truth_signature']]] append_to_stamp = false - stamp(desired_stamps, stamped_template_path, append_to_stamp) + verify(stamped_template_path) { stamp(desired_stamps, stamped_template_path, append_to_stamp) } end def self.stamp264555(stamped_template_path, form) @@ -55,7 +55,7 @@ def self.stamp214142(stamped_template_path, form) { type: :new_page } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) # This is a one-off case where we need to stamp a date on the first page of 21-4142 when resubmitting if form.data['in_progress_form_created_at'] @@ -74,7 +74,7 @@ def self.stamp214142_date_stamp_for_resubmission(stamped_template_path, date_tit { type: :new_page } ] - multistamp(stamped_template_path, date_title, page_configuration, 12) + verified_multistamp(stamped_template_path, date_title, page_configuration, 12) page_configuration = [ { type: :text, position: date_text_stamp_position }, @@ -82,7 +82,7 @@ def self.stamp214142_date_stamp_for_resubmission(stamped_template_path, date_tit { type: :new_page } ] - multistamp(stamped_template_path, date_text, page_configuration, 12) + verified_multistamp(stamped_template_path, date_text, page_configuration, 12) end def self.stamp2110210(stamped_template_path, form) @@ -94,7 +94,7 @@ def self.stamp2110210(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp210845(stamped_template_path, form) @@ -106,7 +106,7 @@ def self.stamp210845(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp21p0847(stamped_template_path, form) @@ -117,7 +117,7 @@ def self.stamp21p0847(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp210972(stamped_template_path, form) @@ -129,7 +129,7 @@ def self.stamp210972(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp210966(stamped_template_path, form) @@ -140,7 +140,7 @@ def self.stamp210966(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp2010207(stamped_template_path, form) @@ -162,7 +162,7 @@ def self.stamp2010207(stamped_template_path, form) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, signature_text, page_configuration) + verified_multistamp(stamped_template_path, signature_text, page_configuration) end def self.stamp4010007_uuid(uuid) @@ -173,7 +173,7 @@ def self.stamp4010007_uuid(uuid) { type: :text, position: desired_stamps[0] } ] - multistamp(stamped_template_path, uuid, page_configuration, 7) + verified_multistamp(stamped_template_path, uuid, page_configuration, 7) end def self.multistamp(stamped_template_path, signature_text, page_configuration, font_size = 16) @@ -200,9 +200,8 @@ def self.multistamp(stamped_template_path, signature_text, page_configuration, f def self.stamp(desired_stamps, stamped_template_path, append_to_stamp, text_only: true) current_file_path = stamped_template_path desired_stamps.each do |x, y, text| - out_path = CentralMail::DatestampPdf.new(current_file_path, append_to_stamp:).run(text:, x:, y:, text_only:, - size: 9) - current_file_path = out_path + datestamp_instance = CentralMail::DatestampPdf.new(current_file_path, append_to_stamp:) + current_file_path = datestamp_instance.run(text:, x:, y:, text_only:, size: 9) end File.rename(current_file_path, stamped_template_path) end @@ -225,16 +224,32 @@ def self.stamp_submission_date(stamped_template_path, config) page_configuration = default_page_configuration page_configuration[config[:page_number]] = { type: :text, position: date_title_stamp_position } - multistamp(stamped_template_path, SUBMISSION_DATE_TITLE, page_configuration, 12) + verified_multistamp(stamped_template_path, SUBMISSION_DATE_TITLE, page_configuration, 12) page_configuration = default_page_configuration page_configuration[config[:page_number]] = { type: :text, position: date_text_stamp_position } - multistamp(stamped_template_path, Time.current.in_time_zone('UTC').strftime('%H:%M %Z %D'), page_configuration, - 12) + current_time = Time.current.in_time_zone('UTC').strftime('%H:%M %Z %D') + verified_multistamp(stamped_template_path, current_time, page_configuration, 12) end end + def self.verify(template_path) + orig_size = File.size(template_path) + yield + stamped_size = File.size(template_path) + + raise StandardError, 'The PDF remained unchanged upon stamping.' unless stamped_size > orig_size + rescue => e + raise StandardError, "An error occurred while verifying stamp: #{e}" + end + + def self.verified_multistamp(stamped_template_path, stamp_text, page_configuration, *) + raise StandardError, 'The provided stamp content was empty.' if stamp_text.blank? + + verify(stamped_template_path) { multistamp(stamped_template_path, stamp_text, page_configuration, *) } + end + def self.default_page_configuration [ { type: :new_page }, diff --git a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21P_0847.json b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21P_0847.json index d274b0ddb0f..805624bc616 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21P_0847.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21P_0847.json @@ -30,5 +30,6 @@ "relationship_to_veteran": "other", "otherRelationship_to_veteran": "friend of a friend" }, - "additional_information": "Lots of \"extra\" stuff here" + "additional_information": "Lots of \"extra\" stuff here", + "statement_of_truth_signature": "John Veteran" } diff --git a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_0845.json b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_0845.json index 1f44b104b94..2e6769e34c6 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_0845.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_0845.json @@ -44,5 +44,6 @@ "release_duration": "untilDate", "release_end_date": "2033-06-16", "security_question": "motherBirthplace", - "security_answer": "Las Vegas, NV" -} \ No newline at end of file + "security_answer": "Las Vegas, NV", + "statement_of_truth_signature": "John Veteran" +} diff --git a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_10210.json b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_10210.json index 2a60ad76f32..c8f7f3e707b 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_10210.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_10210.json @@ -53,5 +53,6 @@ }, "witness_other_relationship_to_claimant": "Other text", "claimant_type": "non-veteran", - "claim_ownership": "third-party" -} \ No newline at end of file + "claim_ownership": "third-party", + "statement_of_truth_signature": "John Veteran" +} diff --git a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_4142.json b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_4142.json index 9ecd0ea59eb..fee3abb9e15 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_4142.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_21_4142.json @@ -138,5 +138,6 @@ "preparer_organization": "Top Org", "court_appointment_info": "Representing \"court stuff\" like...Representing court stuff like...Representing court stuff like...Representing court stuff like...Representing court stuff like...Representing court stuff like...", "relationship_to_veteran": "Veteran Service Officer" - } + }, + "statement_of_truth_signature": "John Veteran" } diff --git a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_unhandled.json b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_unhandled.json index 264454c7afb..84a9daaf2a1 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_unhandled.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/form_with_dangerous_characters_unhandled.json @@ -59,5 +59,6 @@ "postal_code": "54890" } }, - "remarks": "Lengthy \"remarks\" here \nabout what is needed\tand such" + "remarks": "Lengthy \"remarks\" here \nabout what is needed\tand such", + "statement_of_truth_signature": "John Veteran" } diff --git a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_0845.json b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_0845.json index f70216a3970..61d5e28265a 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_0845.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_0845.json @@ -36,5 +36,6 @@ "release_duration": "untilDate", "release_end_date": "2033-06-16", "security_question": "motherBirthplace", - "security_answer": "Las Vegas, NV" + "security_answer": "Las Vegas, NV", + "statement_of_truth_signature": "John M Veteran" } diff --git a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210-min.json b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210-min.json index cac1eca116f..4ed3b48f7fa 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210-min.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210-min.json @@ -16,5 +16,6 @@ "state": "CA", "postal_code": "12345" }, - "veteran_phone": "555-555-5557" -} \ No newline at end of file + "veteran_phone": "555-555-5557", + "statement_of_truth_signature": "Arthur C Preparer" +} diff --git a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210.json b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210.json index 58faea2d302..a81b640091f 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/vba_21_10210.json @@ -53,5 +53,6 @@ }, "witness_other_relationship_to_claimant": "Other text", "claimant_type": "non-veteran", - "claim_ownership": "third-party" -} \ No newline at end of file + "claim_ownership": "third-party", + "statement_of_truth_signature": "Joe Center Claimant" +} diff --git a/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847-min.json b/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847-min.json index 83febd1e180..8f5651e8163 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847-min.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847-min.json @@ -22,5 +22,6 @@ "veteran_ssn": "999442222", "relationship_to_deceased_claimant": { "relationship_to_veteran": "spouse" - } + }, + "statement_of_truth_signature": "Arthur C Preparer" } diff --git a/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847.json b/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847.json index ac311237602..281ef545f63 100644 --- a/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847.json +++ b/modules/simple_forms_api/spec/fixtures/form_json/vba_21p_0847.json @@ -30,5 +30,6 @@ "relationship_to_veteran": "other", "otherRelationship_to_veteran": "friend of a friend" }, - "additional_information": "Lots of extra stuff here" + "additional_information": "Lots of extra stuff here", + "statement_of_truth_signature": "Arthur C Preparer" } diff --git a/modules/simple_forms_api/spec/services/pdf_filler_spec.rb b/modules/simple_forms_api/spec/services/pdf_filler_spec.rb index 9e1c40752b1..1cfb14fb5fe 100644 --- a/modules/simple_forms_api/spec/services/pdf_filler_spec.rb +++ b/modules/simple_forms_api/spec/services/pdf_filler_spec.rb @@ -5,18 +5,21 @@ describe SimpleFormsApi::PdfFiller do def self.test_pdf_fill(form_number, test_payload = form_number) - it 'fills out a PDF from a templated JSON file' do - expected_pdf_path = "tmp/#{form_number}-tmp.pdf" + form_name = form_number.split(Regexp.union(%w[vba_ vha_]))[1].gsub('_', '-') + context "when filling the pdf for form #{form_name} given template #{test_payload}" do + it 'fills out a PDF from a templated JSON file' do + expected_pdf_path = "tmp/#{form_number}-tmp.pdf" - # remove the pdf if it already exists - FileUtils.rm_f(expected_pdf_path) + # remove the pdf if it already exists + FileUtils.rm_f(expected_pdf_path) - # fill the PDF - data = JSON.parse(File.read("modules/simple_forms_api/spec/fixtures/form_json/#{test_payload}.json")) - form = "SimpleFormsApi::#{form_number.titleize.gsub(' ', '')}".constantize.new(data) - filler = SimpleFormsApi::PdfFiller.new(form_number:, form:) - filler.generate - expect(File.exist?(expected_pdf_path)).to eq(true) + # fill the PDF + data = JSON.parse(File.read("modules/simple_forms_api/spec/fixtures/form_json/#{test_payload}.json")) + form = "SimpleFormsApi::#{form_number.titleize.gsub(' ', '')}".constantize.new(data) + filler = SimpleFormsApi::PdfFiller.new(form_number:, form:) + filler.generate + expect(File.exist?(expected_pdf_path)).to eq(true) + end end end diff --git a/modules/simple_forms_api/spec/services/pdf_stamper_spec.rb b/modules/simple_forms_api/spec/services/pdf_stamper_spec.rb index f5fdfc6bfaf..09927ed66fb 100644 --- a/modules/simple_forms_api/spec/services/pdf_stamper_spec.rb +++ b/modules/simple_forms_api/spec/services/pdf_stamper_spec.rb @@ -4,25 +4,150 @@ require SimpleFormsApi::Engine.root.join('spec', 'spec_helper.rb') describe SimpleFormsApi::PdfStamper do - def self.test_pdf_stamp_error(stamp_method, test_payload) - it 'raises an error when generating stamped file' do + let(:data) { JSON.parse(File.read("modules/simple_forms_api/spec/fixtures/form_json/#{test_payload}.json")) } + let(:form) { "SimpleFormsApi::#{test_payload.titleize.gsub(' ', '')}".constantize.new(data) } + let(:path) { 'tmp/stuff.json' } + + describe 'form-specific stamp methods' do + subject(:stamp) { described_class.send(stamp_method, generated_form_path, form) } + + before do allow(Common::FileHelpers).to receive(:random_file_path).and_return('fake/stamp_path') allow(Common::FileHelpers).to receive(:delete_file_if_exists) - allow(Prawn::Document).to receive(:generate).and_raise('Error generating stamped file') + end + + %w[21-4142 21-10210 21p-0847].each do |form_number| + context "when generating a stamped file for form #{form_number}" do + let(:stamp_method) { "stamp#{form_number.gsub('-', '')}" } + let(:test_payload) { "vba_#{form_number.gsub('-', '_')}" } + let(:generated_form_path) { 'fake/generated_form_path' } + + it 'raises an error' do + expect { stamp }.to raise_error(StandardError, /An error occurred while verifying stamp/) + end + end + end + end + + describe '.stamp107959f1' do + subject(:stamp107959f1) { described_class.stamp107959f1(path, form) } + + before do + allow(described_class).to receive(:stamp).and_return(true) + allow(File).to receive(:size).and_return(1, 2) + end + + context 'when statement_of_truth_signature is provided' do + before { stamp107959f1 } + + let(:test_payload) { 'vha_10_7959f_1' } + let(:signature) { form.data['statement_of_truth_signature'] } + let(:stamps) { [[26, 82.5, signature]] } + + it 'calls stamp with correct desired_stamp' do + expect(described_class).to have_received(:stamp).with(stamps, path, false) + end + end + end + + describe '.stamp264555' do + subject(:stamp264555) { described_class.stamp264555(path, form) } + + before do + allow(described_class).to receive(:stamp).and_return(true) + allow(File).to receive(:size).and_return(1, 2) + end + + context 'when it is called with legitimate parameters' do + before { stamp264555 } + + let(:test_payload) { 'vba_26_4555' } + let(:stamps) { [] } + + it 'calls stamp correctly' do + expect(described_class).to have_received(:stamp).with(stamps, path, false) + end + end + end + + describe '.stamp210845' do + subject(:stamp210845) { described_class.stamp210845(path, form) } - generated_form_path = 'fake/generated_form_path' - data = JSON.parse(File.read("modules/simple_forms_api/spec/fixtures/form_json/#{test_payload}.json")) - form = "SimpleFormsApi::#{test_payload.titleize.gsub(' ', '')}".constantize.new(data) + before do + allow(described_class).to receive(:multistamp).and_return(true) + allow(File).to receive(:size).and_return(1, 2) + end + + context 'when it is called with legitimate parameters' do + before { stamp210845 } - expect do - SimpleFormsApi::PdfStamper.send(stamp_method, generated_form_path, form) - end.to raise_error(RuntimeError, 'Error generating stamped file') + let(:test_payload) { 'vba_21_0845' } + let(:signature) { form.data['statement_of_truth_signature'] } + let(:page_config) do + [ + { type: :new_page }, + { type: :new_page }, + { type: :text, position: [50, 240] } + ] + end - expect(Common::FileHelpers).to have_received(:delete_file_if_exists).with('fake/stamp_path') + it 'calls multistamp correctly' do + expect(described_class).to have_received(:multistamp).with(path, signature, page_config) + end end end - test_pdf_stamp_error 'stamp214142', 'vba_21_4142' - test_pdf_stamp_error 'stamp2110210', 'vba_21_10210' - test_pdf_stamp_error 'stamp21p0847', 'vba_21p_0847' + describe '.verify' do + subject(:verify) { described_class.verify('template_path') { double } } + + before { allow(File).to receive(:size).and_return(orig_size, stamped_size) } + + describe 'when verifying a stamp' do + let(:orig_size) { 10_000 } + + context 'when the stamped file size is larger than the original' do + let(:stamped_size) { orig_size + 1 } + + it 'succeeds' do + expect { verify }.not_to raise_error + end + end + + context 'when the stamped file size is the same as the original' do + let(:stamped_size) { orig_size } + + it 'raises an error message' do + expect { verify }.to raise_error( + 'An error occurred while verifying stamp: The PDF remained unchanged upon stamping.' + ) + end + end + + context 'when the stamped file size is less than the original' do + let(:stamped_size) { orig_size - 1 } + + it 'raises an error message' do + expect { verify }.to raise_error( + 'An error occurred while verifying stamp: The PDF remained unchanged upon stamping.' + ) + end + end + end + end + + describe '.verified_multistamp' do + subject(:verified_multistamp) { described_class.verified_multistamp(path, signature_text, config) } + + before { allow(described_class).to receive(:verify).and_return(true) } + + context 'when signature_text is blank' do + let(:path) { nil } + let(:signature_text) { nil } + let(:config) { nil } + + it 'raises an error' do + expect { verified_multistamp }.to raise_error('The provided stamp content was empty.') + end + end + end end diff --git a/modules/vaos/app/services/vaos/v2/systems_service.rb b/modules/vaos/app/services/vaos/v2/systems_service.rb index 7b8da35423a..32503b2e1a1 100644 --- a/modules/vaos/app/services/vaos/v2/systems_service.rb +++ b/modules/vaos/app/services/vaos/v2/systems_service.rb @@ -3,8 +3,6 @@ module VAOS module V2 class SystemsService < VAOS::SessionService - STOP_CODE_FILTERS = :va_online_scheduling_clinic_filtering - def get_facility_clinics(location_id:, clinical_service: nil, clinic_ids: nil, @@ -21,11 +19,9 @@ def get_facility_clinics(location_id:, 'pageNumber' => page_number }.compact - # 'clinicalService' is used to retrieve clinics for appointment scheduling, + # 'clinicalService' is used when retrieving clinics for appointment scheduling, # triggering stop code filtering to avoid displaying unavailable clinics. - if url_params['clinicalService'].present? && Flipper.enabled?(STOP_CODE_FILTERS, user) - url_params.merge!('enableStopCodeFilter' => true) - end + url_params.merge!('enableStopCodeFilter' => true) if url_params['clinicalService'].present? response = perform(:get, url, url_params, headers) response.body[:data].map { |clinic| OpenStruct.new(clinic) } diff --git a/modules/vba_documents/app/sidekiq/vba_documents/upload_processor.rb b/modules/vba_documents/app/sidekiq/vba_documents/upload_processor.rb index 43a35c36ffb..6909fec42f0 100644 --- a/modules/vba_documents/app/sidekiq/vba_documents/upload_processor.rb +++ b/modules/vba_documents/app/sidekiq/vba_documents/upload_processor.rb @@ -21,8 +21,6 @@ class UploadProcessor sidekiq_options unique_for: 30.days def perform(guid, caller_data, retries = 0) - return if cancelled? - response = nil brt = Benchmark.realtime do # @retries variable used via the CentralMail::Utilities which is included via VBADocuments::UploadValidations @@ -46,20 +44,6 @@ def perform(guid, caller_data, retries = 0) response&.success? ? true : false end - def cancelled? - Sidekiq.redis do |c| - if c.respond_to? :exists? - c.exists?("cancelled-#{jid}") - else - c.exists("cancelled-#{jid}") - end - end - end - - def self.cancel!(jid) - Sidekiq.redis { |c| c.setex("cancelled-#{jid}", 86_400, 1) } - end - private # rubocop:disable Metrics/MethodLength diff --git a/modules/vba_documents/spec/request/v1/uploads_request_spec.rb b/modules/vba_documents/spec/request/v1/uploads_request_spec.rb index aec54da50f3..7553ff67406 100644 --- a/modules/vba_documents/spec/request/v1/uploads_request_spec.rb +++ b/modules/vba_documents/spec/request/v1/uploads_request_spec.rb @@ -135,7 +135,6 @@ @md = JSON.parse(valid_metadata) @upload_submission = VBADocuments::UploadSubmission.new @upload_submission.update(status: 'uploaded') - allow_any_instance_of(VBADocuments::UploadProcessor).to receive(:cancelled?).and_return(false) allow_any_instance_of(Tempfile).to receive(:size).and_return(1) # must be > 0 or submission will error w/DOC107 allow(VBADocuments::MultipartParser).to receive(:parse) { { 'metadata' => @md.to_json, 'content' => valid_doc } diff --git a/modules/vba_documents/spec/request/v2/uploads_request_spec.rb b/modules/vba_documents/spec/request/v2/uploads_request_spec.rb index 4a4e2646cb9..5d74d97bdbd 100644 --- a/modules/vba_documents/spec/request/v2/uploads_request_spec.rb +++ b/modules/vba_documents/spec/request/v2/uploads_request_spec.rb @@ -202,7 +202,6 @@ @md = JSON.parse(valid_metadata) @upload_submission = VBADocuments::UploadSubmission.new @upload_submission.update(status: 'uploaded') - allow_any_instance_of(VBADocuments::UploadProcessor).to receive(:cancelled?).and_return(false) allow_any_instance_of(Tempfile).to receive(:size).and_return(1) # must be > 0 or submission will error w/DOC107 allow(VBADocuments::MultipartParser).to receive(:parse) { { 'metadata' => @md.to_json, 'content' => valid_doc } diff --git a/modules/vba_documents/spec/sidekiq/upload_processor_spec.rb b/modules/vba_documents/spec/sidekiq/upload_processor_spec.rb index c032d7aa89c..5c6294272eb 100644 --- a/modules/vba_documents/spec/sidekiq/upload_processor_spec.rb +++ b/modules/vba_documents/spec/sidekiq/upload_processor_spec.rb @@ -72,7 +72,6 @@ end before do - allow_any_instance_of(described_class).to receive(:cancelled?).and_return(false) allow_any_instance_of(Tempfile).to receive(:size).and_return(1) # must be > 0 or submission will error w/DOC107 objstore = instance_double(VBADocuments::ObjectStore) version = instance_double(Aws::S3::ObjectVersion) diff --git a/modules/vye/app/models/vye/address_change.rb b/modules/vye/app/models/vye/address_change.rb index 1f55b44bcb3..5394939ed19 100644 --- a/modules/vye/app/models/vye/address_change.rb +++ b/modules/vye/app/models/vye/address_change.rb @@ -4,18 +4,24 @@ module Vye class Vye::AddressChange < ApplicationRecord belongs_to :user_info - ENCRYPTED_ATTRIBUTES = %i[ - veteran_name address1 address2 address3 address4 city state zip_code - ].freeze - has_kms_key - has_encrypted(*ENCRYPTED_ATTRIBUTES, key: :kms_key, **lockbox_options) - REQUIRED_ATTRIBUTES = %i[ - veteran_name address1 city state - ].freeze + has_encrypted( + :veteran_name, + :address1, :address2, :address3, :address4, :address5, + :city, :state, :zip_code, + key: :kms_key, **lockbox_options + ) + + validates( + :veteran_name, :address1, :city, :state, + presence: true, if: -> { origin == 'frontend' } + ) - validates(*REQUIRED_ATTRIBUTES, presence: true) + validates( + :veteran_name, :address1, + presence: true, if: -> { origin == 'backend' } + ) enum origin: { frontend: 'f', backend: 'b' } diff --git a/modules/vye/app/models/vye/award.rb b/modules/vye/app/models/vye/award.rb index 09682f9cc11..f37db8962a3 100644 --- a/modules/vye/app/models/vye/award.rb +++ b/modules/vye/app/models/vye/award.rb @@ -6,11 +6,12 @@ class Vye::Award < ApplicationRecord enum cur_award_ind: { current: 'C', future: 'F', past: 'P' } - REQUIRED_ATTRIBUTES = %i[ - award_begin_date award_end_date begin_rsn cur_award_ind end_rsn - monthly_rate number_hours payment_date training_time type_hours type_training - ].freeze - - validates(*REQUIRED_ATTRIBUTES, presence: true) + validates( + *%i[ + award_end_date cur_award_ind end_rsn + monthly_rate number_hours payment_date training_time + ].freeze, + presence: true + ) end end diff --git a/modules/vye/app/models/vye/user_info.rb b/modules/vye/app/models/vye/user_info.rb index 59c30346162..3d94440ca00 100644 --- a/modules/vye/app/models/vye/user_info.rb +++ b/modules/vye/app/models/vye/user_info.rb @@ -4,11 +4,16 @@ module Vye class Vye::UserInfo < ApplicationRecord INCLUDES = %i[address_changes awards pending_documents verifications].freeze - self.ignored_columns += %i[ - address_line2_ciphertext address_line3_ciphertext address_line4_ciphertext - address_line5_ciphertext address_line6_ciphertext - full_name_ciphertext icn ssn_digest suffix zip_ciphertext - ] + self.ignored_columns += + [ + :ssn_digest, :icn, # moved to UserProfile + + :suffix, # not needed + + :address_line2_ciphertext, :address_line3_ciphertext, # moved to AddressChange + :address_line4_ciphertext, :address_line5_ciphertext, # moved to AddressChange + :address_line6_ciphertext, :full_name_ciphertext, :zip_ciphertext # moved to AddressChange + ] belongs_to :user_profile @@ -28,12 +33,12 @@ class Vye::UserInfo < ApplicationRecord delegate :icn, to: :user_profile, allow_nil: true delegate :pending_documents, to: :user_profile, allow_nil: true - %i[dob file_number ssn stub_nm].freeze.tap do |attributes| - has_kms_key - has_encrypted(*attributes, key: :kms_key, **lockbox_options) + has_kms_key + has_encrypted(:file_number, :ssn, :dob, :stub_nm, key: :kms_key, **lockbox_options) - validates(*attributes, presence: true) - end + validates :dob, :stub_nm, presence: true + + validate :ssn_or_file_number_present validates( :cert_issue_date, :date_last_certified, :del_date, :fac_code, :indicator, @@ -45,6 +50,12 @@ def verification_required verifications.empty? end + def ssn_or_file_number_present + return true if ssn.present? || file_number.present? + + errors.add(:base, 'Either SSN or file number must be present.') + end + scope :with_assos, -> { includes(:address_changes, :awards, user_profile: :pending_documents) } end end diff --git a/modules/vye/app/models/vye/user_profile.rb b/modules/vye/app/models/vye/user_profile.rb index 810cd822721..8f930ccc93d 100644 --- a/modules/vye/app/models/vye/user_profile.rb +++ b/modules/vye/app/models/vye/user_profile.rb @@ -10,7 +10,7 @@ class Vye::UserProfile < ApplicationRecord digest_attribute :ssn digest_attribute :file_number - validates :ssn_digest, :file_number_digest, presence: true + validate :ssn_or_file_number_present scope :with_assos, -> { includes(:pending_documents, :user_infos) } @@ -25,4 +25,12 @@ def self.find_and_update_icn(user:) result&.update!(icn: user.icn) end end + + private + + def ssn_or_file_number_present + return true if ssn_digest.present? || file_number_digest.present? + + errors.add(:base, 'Either SSN or file number must be present.') + end end diff --git a/modules/vye/config/bdn_line_extraction_config.yaml b/modules/vye/config/bdn_line_extraction_config.yaml new file mode 100644 index 00000000000..c174983ce7f --- /dev/null +++ b/modules/vye/config/bdn_line_extraction_config.yaml @@ -0,0 +1,61 @@ +--- +:main_line: + :ssn: 9 + :file_number: 9 + :suffix: 2 + :dob: 8 + :mr_status: 1 + :rem_ent: 7 + :cert_issue_date: 8 + :del_date: 8 + :date_last_certified: 8 + :veteran_name: 20 + :address1: 20 + :address2: 20 + :address3: 20 + :address4: 20 + :address5: 20 + :zip_code: 9 + :stub_nm: 7 + :rpo_code: 3 + :fac_code: 8 + :payment_amt: 7 +:award_line: + :award_begin_date: 8 + :award_end_date: 8 + :training_time: 1 + :payment_date: 8 + :monthly_rate: 7 + :begin_rsn: 2 + :end_rsn: 2 + :type_training: 1 + :number_hours: 2 + :type_hours: 1 + :cur_award_ind: 1 +:mappings: + :profile: + - :ssn + - :file_number + :info: + - :ssn + - :file_number + - :dob + - :mr_status + - :rem_ent + - :cert_issue_date + - :del_date + - :date_last_certified + - :stub_nm + - :rpo_code + - :fac_code + - :payment_amt + - :indicator + :address: + - :veteran_name + - :address1 + - :address2 + - :address3 + - :address4 + - :address5 + - :zip_code +:indicator: true diff --git a/modules/vye/lib/vye/batch_transfer/ingress_files.rb b/modules/vye/lib/vye/batch_transfer/ingress_files.rb index aab2dc7a7c0..a744822b31f 100644 --- a/modules/vye/lib/vye/batch_transfer/ingress_files.rb +++ b/modules/vye/lib/vye/batch_transfer/ingress_files.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module VYE +module Vye module BatchTransfer module IngressFiles module_function @@ -10,6 +10,20 @@ module IngressFiles def bdn_feed_filename = BDN_FEED_FILENAME def tims_feed_filename = TIMS_FEED_FILENAME + + def bdn_import(data) + data.each_line do |line| + parsed = BdnLineExtraction.new(line: line.chomp, result: {}, award_lines: [], awards: []) + + profile = Vye::UserProfile.build(parsed.attributes[:profile]) + info = profile.user_infos.build(parsed.attributes[:info]) + info.address_changes.build({ origin: 'backend' }.merge(parsed.attributes[:address])) + parsed.attributes[:awards].each do |award| + info.awards.build(award) + end + profile.save! + end + end end end end diff --git a/modules/vye/lib/vye/batch_transfer/ingress_files/bdn_line_extraction.rb b/modules/vye/lib/vye/batch_transfer/ingress_files/bdn_line_extraction.rb new file mode 100644 index 00000000000..c8469d538ac --- /dev/null +++ b/modules/vye/lib/vye/batch_transfer/ingress_files/bdn_line_extraction.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Vye + module BatchTransfer + module IngressFiles + BdnLineExtraction = Struct.new(:line, :result, :award_lines, :awards) do + def initialize(line:, result:, award_lines:, awards:) + super + + extract_main + extract_award_lines + extract_indicator + extract_awards + end + + def config + @config ||= YAML.load_file Vye::Engine.root / 'config/bdn_line_extraction_config.yaml' + end + + def extract_main + config[:main_line] + .each do |field, length| + extracted = line.slice!(0...length).strip + result.update(field => extracted) + end + end + + def extract_award_lines + (0...4) + .each do |_i| + dead = line[0...8].strip == '' + extracted = line.slice!(0...41) + award_lines << extracted unless dead + end + end + + def extract_awards + # iterate over the fields for each award line and extract the data + # think list comprehensions in python, haskell, or erlang + award_lines + .each_with_index.to_a + .product(config[:award_line].each_pair.to_a) + .each do |(award_line, i), (field, length)| + extracted = award_line.slice!(0...length).strip + awards[i] ||= {} + awards[i].update(field => extracted) + end + end + + def extract_indicator + return unless config[:indicator] + + result.update(indicator: line.slice!(0...1).strip) + end + + def attributes + raise 'incomplete extraction' unless line.blank? && award_lines.all?(&:blank?) + + profile = result.slice(*config[:mappings][:profile]) + info = result.slice(*config[:mappings][:info]) + address = result.slice(*config[:mappings][:address]) + + { profile:, info:, address:, awards: } + end + end + end + end +end diff --git a/modules/vye/lib/vye/engine.rb b/modules/vye/lib/vye/engine.rb index dd0bc342d93..2310151db33 100644 --- a/modules/vye/lib/vye/engine.rb +++ b/modules/vye/lib/vye/engine.rb @@ -6,6 +6,7 @@ module Vye class Engine < Rails::Engine isolate_namespace Vye config.generators.api_only = true + config.autoload_paths << (root / 'lib') initializer 'model_core.factories', after: 'factory_bot.set_factory_paths' do FactoryBot.definition_file_paths << File.expand_path('../../spec/factories', __dir__) if defined?(FactoryBot) diff --git a/modules/vye/spec/factories/vye/address_changes.rb b/modules/vye/spec/factories/vye/address_changes.rb index 4614d1e8958..40bc66818a8 100644 --- a/modules/vye/spec/factories/vye/address_changes.rb +++ b/modules/vye/spec/factories/vye/address_changes.rb @@ -7,5 +7,6 @@ city { Faker::Address.city } state { Faker::Address.state_abbr } zip_code { Faker::Address.zip_code } + origin { Vye::AddressChange.origins['frontend'] } end end diff --git a/modules/vye/spec/fixtures/bdn_sample/WAVE.txt b/modules/vye/spec/fixtures/bdn_sample/WAVE.txt new file mode 100755 index 00000000000..a6e7d15c57e --- /dev/null +++ b/modules/vye/spec/fixtures/bdn_sample/WAVE.txt @@ -0,0 +1 @@ +123456789 19800101E3600000198603281996020519860328JOHN APPLESEED 1 Mockingbird Ln APT 1 Houston TX 77401 JAPPLES316119071110011550 00000000198603281198603280003500 66 00 C A diff --git a/modules/vye/spec/lib/vye/batch_transfer/ingress_files_spec.rb b/modules/vye/spec/lib/vye/batch_transfer/ingress_files_spec.rb index bd396a66e54..735cea37c76 100644 --- a/modules/vye/spec/lib/vye/batch_transfer/ingress_files_spec.rb +++ b/modules/vye/spec/lib/vye/batch_transfer/ingress_files_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' require 'vye/batch_transfer/ingress_files' -RSpec.describe VYE::BatchTransfer::IngressFiles do +RSpec.describe Vye::BatchTransfer::IngressFiles do describe '#bdn_feed_filename' do it 'returns a string' do expect(described_class.bdn_feed_filename).to be_a(String) @@ -15,4 +15,11 @@ expect(described_class.tims_feed_filename).to be_a(String) end end + + it 'imports lines from BDN extract' do + data = Vye::Engine.root / 'spec/fixtures/bdn_sample/WAVE.txt' + expect do + described_class.bdn_import(data) + end.not_to raise_error + end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 60eb403fdce..0ff3e3437b1 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -478,15 +478,6 @@ def append_info_to_payload(payload) expect(controller.payload[:user_uuid]).to eq(user.uuid) end - context 'with a virtual host that is invalid' do - let(:header_host_value) { 'unsafe_host' } - - it 'returns bad request' do - get :test_authentication - expect(response).to have_http_status(:bad_request) - end - end - context 'with a credential that is locked' do let(:user) { build(:user, :loa3, :idme_lock) } diff --git a/spec/factories/burial_claim.rb b/spec/factories/burial_claim.rb index 8d8705b75aa..23c7189318d 100644 --- a/spec/factories/burial_claim.rb +++ b/spec/factories/burial_claim.rb @@ -33,6 +33,39 @@ end end + factory :burial_claim_v2, class: 'SavedClaim::Burial' do + form_id { '21P-530V2' } + form do + { + privacyAgreementAccepted: true, + veteranFullName: { + first: 'WESLEY', + last: 'FORD' + }, + claimantEmail: 'foo@foo.com', + deathDate: '1989-12-13', + veteranDateOfBirth: '1986-05-06', + veteranSocialSecurityNumber: '796043735', + claimantAddress: { + country: 'USA', + state: 'CA', + postalCode: '90210', + street: '123 Main St', + city: 'Anytown' + }, + claimantFullName: { + first: 'Derrick', + middle: 'A', + last: 'Stewart' + }, + burialAllowance: true, + plotAllowance: true, + transportation: true, + formV2: true + }.to_json + end + end + # Bad names that fail the EMMS API regex factory :burial_claim_bad_names, class: 'SavedClaim::Burial' do form_id { '21P-530' } diff --git a/spec/fixtures/pdf_fill/21P-530V2/kitchen_sink.json b/spec/fixtures/pdf_fill/21P-530V2/kitchen_sink.json new file mode 100644 index 00000000000..f4bc77bd629 --- /dev/null +++ b/spec/fixtures/pdf_fill/21P-530V2/kitchen_sink.json @@ -0,0 +1,56 @@ +{ + "transportationExpenses":true, + "plotExpenseResponsibility":true, + "govtContributions":true, + "amountGovtContribution":"50000", + "cemeteryLocation":{"name":"state cemetery", "zip":"04102"}, + "cemetaryLocationQuestion":"cemetery", + "nationalOrFederal":true, + "name":"name of cemetery", + "finalRestingPlace":{"location":"other", "other":"other"}, + "previouslyReceivedAllowance":true, + "burialExpenseResponsibility":true, + "confirmation":{"checkBox":true}, + "burialAllowanceRequested":{"service":true, "nonService":true, "unclaimed":true}, + "burialAllowance":true, + "plotAllowance":true, + "processOption":false, + "transportation":true, + "previousNames":[{"first":"previous", "middle":"other", "last":"name", "suffix":"Jr.", "serviceBranch":"navy"}], + "militaryServiceNumber":"123123", + "toursOfDuty": + [{"serviceBranch":"Air Force", + "dateRange":{"from":"1970-01-01", "to":"1974-01-01"}, + "placeOfEntry":"placeofentry12", + "placeOfSeparation":"placeofentry12", + "rank":"gradeofentry12", + "unit":"unit"}, + {"serviceBranch":"Army", + "dateRange":{"from":"1976-01-01", "to":"1978-01-02"}, + "placeOfEntry":"placeofentry34", + "placeOfSeparation":"placeofentry34", + "rank":"gradeofentry13", + "unit":"unit"}, + {"serviceBranch":"Navy", + "dateRange":{"from":"1980-01-01", "to":"1984-01-01"}, + "placeOfEntry":"placeofentry56", + "placeOfSeparation":"placeofentry56", + "rank":"gradeofentry56", + "unit":"unit"}], + "locationOfDeath":{"location":"other", "other":"other place"}, + "deathDate":"2024-01-01", + "burialDate":"2024-01-02", + "veteranFullName":{"first":"veteran", "middle":"middle", "last":"lastname", "suffix":"Jr."}, + "veteranSocialSecurityNumber":"987654322", + "vaFileNumber":"987654322", + "veteranDateOfBirth":"1950-01-01", + "claimantEmail":"test@test.com", + "claimantPhone":"5555555555", + "claimantIntPhone":"5555555556", + "claimantAddress":{"country":"US", "street":"123 fake street", "street2":"street address line 2", "city":"portland", "state":"ME", "postalCode":"04102"}, + "claimantFullName":{"first":"test", "middle":"middle", "last":"spouse", "suffix":"Jr."}, + "claimantSocialSecurityNumber":"987654321", + "claimantDateOfBirth":"1960-01-01", + "formV2":true, + "relationshipToVeteran":"spouse", + "privacyAgreementAccepted":true} \ No newline at end of file diff --git a/spec/fixtures/pdf_fill/21P-530V2/merge_fields.json b/spec/fixtures/pdf_fill/21P-530V2/merge_fields.json new file mode 100644 index 00000000000..108b49025f0 --- /dev/null +++ b/spec/fixtures/pdf_fill/21P-530V2/merge_fields.json @@ -0,0 +1,102 @@ +{ + "transportationExpenses":true, + "plotExpenseResponsibility":true, + "govtContributions":true, + "amountGovtContribution":"50000", + "cemeteryLocation":{"name":"state cemetery", "zip":"04102"}, + "cemetaryLocationQuestion":"cemetery", + "nationalOrFederal":true, + "name":"name of cemetery", + "finalRestingPlace":{"location":{"cemetery":"Off", "privateResidence":"Off", "mausoleum":"Off", "other":"On"}, "other":"other"}, + "previouslyReceivedAllowance":true, + "burialExpenseResponsibility":true, + "confirmation":true, + "burialAllowanceRequested":{"checkbox":{"service":"On", "nonService":"On", "unclaimed":"On"}}, + "burialAllowance":true, + "plotAllowance":true, + "processOption":false, + "transportation":true, + "previousNames":"previous other name Jr. (navy)", + "militaryServiceNumber":"123123", + "toursOfDuty":[ + { + "serviceBranch":"Air Force", + "placeOfEntry":"placeofentry12", + "placeOfSeparation":"placeofentry12", + "rank":"Air Force, gradeofentry12", + "unit":"unit", + "dateRangeStart":"1970-01-01", + "dateRangeEnd":"1974-01-01", + "militaryServiceNumber":"123123" + }, + { + "serviceBranch":"Army", + "placeOfEntry":"placeofentry34", + "placeOfSeparation":"placeofentry34", + "rank":"Army, gradeofentry13", + "unit":"unit", + "dateRangeStart":"1976-01-01", + "dateRangeEnd":"1978-01-02", + "militaryServiceNumber":"123123" + }, + { + "serviceBranch":"Navy", + "placeOfEntry":"placeofentry56", + "placeOfSeparation":"placeofentry56", + "rank":"Navy, gradeofentry56", + "unit":"unit", + "dateRangeStart":"1980-01-01", + "dateRangeEnd":"1984-01-01", + "militaryServiceNumber":"123123" + } + ], + "locationOfDeath": + { + "location":"other", + "other":"other place", + "checkbox":{"other":"On"} + }, + "deathDate":{"month":"01", "day":"01", "year":"2024"}, + "burialDate":{"month":"01", "day":"02", "year":"2024"}, + "veteranFullName":{"first":"veteran", "middle":"middle", "last":"lastname", "suffix":"Jr.", "middleInitial":"m"}, + "veteranSocialSecurityNumber":{"first":"987", "second":"65", "third":"4322"}, + "vaFileNumber":"987654322", + "veteranDateOfBirth":{"month":"01", "day":"01", "year":"1950"}, + "claimantEmail":"test@test.com", + "claimantPhone":{"first":"555", "second":"555", "third":"5555"}, + "claimantIntPhone":"5555555556", + "claimantAddress":{"country":"US", "street":"123 fake street", "street2":"street address line 2", "city":"portland", "state":"ME", "postalCode":{"firstFive":"04102", "lastFour":null}}, + "claimantFullName":{"first":"test", "middle":"middle", "last":"spouse", "suffix":"Jr.", "middleInitial":"m"}, + "claimantSocialSecurityNumber":{"first":"987", "second":"65", "third":"4321"}, + "claimantDateOfBirth":{"month":"01", "day":"01", "year":"1960"}, + "formV2":true, + "relationshipToVeteran":{"spouse":"On", "child":"Off", "executor":"Off", "parent":"Off", "funeralHome":"Off", "other":"Off"}, + "privacyAgreementAccepted":true, + "signature":"test spouse", + "signatureDate":"2024-03-21", + "veteranSocialSecurityNumber2":{"first":"987", "second":"65", "third":"4322"}, + "veteranSocialSecurityNumber3":{"first":"987", "second":"65", "third":"4322"}, + "stateCemeteryOrTribalTrustName":"state cemetery", + "stateCemeteryOrTribalTrustZip":"04102", + "hasBurialExpenseResponsibility":"On", + "noBurialExpenseResponsibility":null, + "hasPlotExpenseResponsibility":"On", + "noPlotExpenseResponsibility":null, + "hasProcessOption":null, + "noProcessOption":"On", + "hasConfirmation":"YES", + "noConfirmation":null, + "cemetaryLocationQuestionCemetery":"On", + "cemetaryLocationQuestionTribal":"Off", + "cemetaryLocationQuestionNone":"Off", + "hasNationalOrFederal":"YES", + "noNationalOrFederal":null, + "hasGovtContributions":"YES", + "noGovtContributions":null, + "hasPreviouslyReceivedAllowance":"YES", + "noPreviouslyReceivedAllowance":null, + "hasAllowanceStatementOfTruth":null, + "noAllowanceStatementOfTruth":null, + "hasTransportation":"YES", + "noTransportation":null +} \ No newline at end of file diff --git a/spec/lib/common/exceptions/not_a_safe_host_error_spec.rb b/spec/lib/common/exceptions/not_a_safe_host_error_spec.rb deleted file mode 100644 index 56ecabfdd32..00000000000 --- a/spec/lib/common/exceptions/not_a_safe_host_error_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Common::Exceptions::NotASafeHostError do - context 'with no attributes provided' do - it do - expect { described_class.new } - .to raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1)') - end - end - - context 'with host provided' do - subject { described_class.new('unsafe_host') } - - it 'implements #errors which returns an array' do - expect(subject.errors).to be_an(Array) - end - - it 'the errors object has all relevant keys' do - expect(subject.errors.first.to_hash) - .to eq(title: 'Bad Request', - detail: '"unsafe_host" is not a safe host', - code: '110', - status: '400') - end - end -end diff --git a/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb b/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb index b95ce03cd19..4f5202d5c3d 100644 --- a/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb +++ b/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb @@ -210,4 +210,38 @@ def provider(api_provider = nil) end.to raise_error NotImplementedError end end + + context 'generate_pdf' do + def provider(api_provider = nil) + ApiProviderFactory.call( + type: ApiProviderFactory::FACTORIES[:generate_pdf], + provider: api_provider, + options: { auth_headers: }, + current_user:, + feature_toggle: ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF + ) + end + + it 'provides an EVSS generate_pdf provider' do + expect(provider(:evss).class).to equal(EvssGeneratePdfProvider) + end + + it 'provides a Lighthouse generate_pdf provider' do + expect(provider(:lighthouse).class).to equal(LighthouseGeneratePdfProvider) + end + + it 'provides generate_pdf provider based on Flipper' do + Flipper.enable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + expect(provider.class).to equal(LighthouseGeneratePdfProvider) + + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + expect(provider.class).to equal(EvssGeneratePdfProvider) + end + + it 'throw error if provider unknown' do + expect do + provider(:random) + end.to raise_error NotImplementedError + end + end end diff --git a/spec/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider_spec.rb b/spec/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider_spec.rb new file mode 100644 index 00000000000..030d63de9d8 --- /dev/null +++ b/spec/lib/disability_compensation/providers/generate_pdf/evss_generate_pdf_provider_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'disability_compensation/factories/api_provider_factory' +require 'disability_compensation/providers/generate_pdf/evss_generate_pdf_provider' +require 'support/disability_compensation_form/shared_examples/generate_pdf_service_provider' + +RSpec.describe EvssGeneratePdfProvider do + let(:current_user) do + create(:user) + end + + let(:auth_headers) do + EVSS::AuthHeaders.new(current_user).to_h + end + + before do + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + end + + after do + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + end + + it_behaves_like 'generate pdf service provider' + + it 'creates a breakered evss service' do + provider = EvssGeneratePdfProvider.new(auth_headers) + expect(provider.instance_variable_get(:@service).class).to equal(EVSS::DisabilityCompensationForm::Service) + + provider = EvssGeneratePdfProvider.new(auth_headers, breakered: true) + expect(provider.instance_variable_get(:@service).class).to equal(EVSS::DisabilityCompensationForm::Service) + end + + it 'creates a non-breakered evss service' do + provider = EvssGeneratePdfProvider.new(auth_headers, breakered: false) + expect(provider.instance_variable_get(:@service).class) + .to equal(EVSS::DisabilityCompensationForm::NonBreakeredService) + end + + it 'retrieves a generated 526 pdf from the EVSS API' do + VCR.use_cassette('form526_backup/200_evss_get_pdf', match_requests_on: %i[uri method]) do + provider = EvssGeneratePdfProvider.new(auth_headers) + response = provider.generate_526_pdf({}.to_json) + expect(response.body['pdf']).to eq('') + end + end +end diff --git a/spec/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider_spec.rb b/spec/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider_spec.rb new file mode 100644 index 00000000000..943f88d3a65 --- /dev/null +++ b/spec/lib/disability_compensation/providers/generate_pdf/generate_pdf_provider_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'disability_compensation/providers/generate_pdf/generate_pdf_provider' + +RSpec.describe GeneratePdfProvider do + it 'always raises an error on the BRDProvider base module' do + expect do + subject.generate_526_pdf({}) + end.to raise_error NotImplementedError + end +end diff --git a/spec/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider_spec.rb b/spec/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider_spec.rb new file mode 100644 index 00000000000..808fb69a552 --- /dev/null +++ b/spec/lib/disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'disability_compensation/factories/api_provider_factory' +require 'disability_compensation/providers/generate_pdf/lighthouse_generate_pdf_provider' +require 'support/disability_compensation_form/shared_examples/generate_pdf_service_provider' +require 'lighthouse/service_exception' + +RSpec.describe LighthouseGeneratePdfProvider do + let(:auth_headers) { {} } + + before do + @provider = LighthouseGeneratePdfProvider.new(auth_headers) + Flipper.enable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + end + + after do + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + end + + it_behaves_like 'generate pdf service provider' + + # TODO: Implement in Ticket# + # it 'retrieves a generated 526 pdf from the Lighthouse API' do + # VCR.use_cassette('lighthouse/benefits_claims/generate_pdf/200_response') do + # + # response = @provider.generate_526_pdf + # expect(response).to eq(nil) + # end + # end +end diff --git a/spec/lib/pdf_fill/forms/va21p530v2_spec.rb b/spec/lib/pdf_fill/forms/va21p530v2_spec.rb new file mode 100644 index 00000000000..36b6947982a --- /dev/null +++ b/spec/lib/pdf_fill/forms/va21p530v2_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pdf_fill/forms/va21p530v2' + +def basic_class + PdfFill::Forms::Va21p530v2.new({}) +end + +describe PdfFill::Forms::Va21p530v2 do + let(:form_data) do + {} + end + + let(:new_form_class) do + described_class.new(form_data) + end + + def class_form_data + new_form_class.instance_variable_get(:@form_data) + end + + test_method( + basic_class, + 'expand_checkbox', + [ + [ + [ + true, 'GovtContribution' + ], + { 'hasGovtContribution' => 'YES', 'noGovtContribution' => nil } + ], + [ + [ + false, 'GovtContribution' + ], + { 'hasGovtContribution' => nil, 'noGovtContribution' => 'NO' } + ], + [ + [ + nil, 'GovtContribution' + ], + { 'hasGovtContribution' => nil, 'noGovtContribution' => nil } + ] + ] + ) + + test_method( + basic_class, + 'split_phone', + [ + [ + [{}, nil], + nil + ], + [ + [ + { phone: '1112223333' }, + :phone + ], + { 'first' => '111', 'second' => '222', 'third' => '3333' } + ] + ] + ) + + describe '#convert_location_of_death' do + subject do + new_form_class.convert_location_of_death + end + + context 'with no location of death' do + it 'returns nil' do + expect(subject).to eq(nil) + end + end + + context 'with a regular location of death' do + let(:form_data) do + { + 'locationOfDeath' => { + 'location' => 'nursingHomeUnpaid' + } + } + end + + it 'returns the directly mapped location' do + subject + expect(class_form_data['locationOfDeath']['checkbox']).to eq({ 'nursingHomeUnpaid' => 'On' }) + end + end + + context 'with a location needed for translation' do + let(:form_data) do + { + 'locationOfDeath' => { + 'location' => 'atHome' + } + } + end + + it 'returns the directly mapped location' do + subject + expect(class_form_data['locationOfDeath']['checkbox']).to eq({ 'nursingHomeUnpaid' => 'On' }) + end + end + end + + describe '#merge_fields' do + it 'merges the right fields', run_at: '2024-03-21 00:00:00 EDT' do + expect(described_class.new(get_fixture('pdf_fill/21P-530V2/kitchen_sink')).merge_fields.to_json).to eq( + get_fixture('pdf_fill/21P-530V2/merge_fields').to_json + ) + end + end +end diff --git a/spec/lib/sidekiq/form526_backup_submission_process/submit_spec.rb b/spec/lib/sidekiq/form526_backup_submission_process/submit_spec.rb index 69ff18ad1ff..0ec092ed2f5 100644 --- a/spec/lib/sidekiq/form526_backup_submission_process/submit_spec.rb +++ b/spec/lib/sidekiq/form526_backup_submission_process/submit_spec.rb @@ -4,12 +4,14 @@ require 'evss/disability_compensation_auth_headers' # required to build a Form526Submission require 'sidekiq/form526_backup_submission_process/submit' +require 'disability_compensation/factories/api_provider_factory' RSpec.describe Sidekiq::Form526BackupSubmissionProcess::Submit, type: :job do subject { described_class } before do Sidekiq::Job.clear_all + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) end let(:user) { FactoryBot.create(:user, :loa3) } diff --git a/spec/lib/sidekiq/form526_job_status_tracker/job_tracker_spec.rb b/spec/lib/sidekiq/form526_job_status_tracker/job_tracker_spec.rb index a8c84527c9b..8ed993a919b 100644 --- a/spec/lib/sidekiq/form526_job_status_tracker/job_tracker_spec.rb +++ b/spec/lib/sidekiq/form526_job_status_tracker/job_tracker_spec.rb @@ -11,6 +11,10 @@ end end + before do + Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_GENERATE_PDF) + end + context 'with an exhausted callback message' do let!(:form526_submission) { create :form526_submission } let!(:form526_job_status) do diff --git a/spec/lib/va_profile/profile/v3/health_benefit_bio_response_spec.rb b/spec/lib/va_profile/profile/v3/health_benefit_bio_response_spec.rb new file mode 100644 index 00000000000..70f51f3ed61 --- /dev/null +++ b/spec/lib/va_profile/profile/v3/health_benefit_bio_response_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'va_profile/profile/v3/health_benefit_bio_response' + +describe VAProfile::Profile::V3::HealthBenefitBioResponse do + subject { described_class.new(response) } + + let(:response) do + double('Faraday::Response', + status: 200, + body: { + 'profile' => { + 'health_benefit' => { + 'associated_persons' => [{ + 'contact_type' => contact_type + }] + } + } + }) + end + + describe 'Emergency contact' do + let(:contact_type) { 'Emergency Contact' } + + it 'includes contact' do + expect(subject.contacts).not_to be_empty + end + end + + describe 'Other emergency contact' do + let(:contact_type) { 'Other emergency contact' } + + it 'includes contact' do + expect(subject.contacts).not_to be_empty + end + end + + describe 'Primary Next of Kin' do + let(:contact_type) { 'Primary Next of Kin' } + + it 'includes contact' do + expect(subject.contacts).not_to be_empty + end + end + + describe 'Other Next of Kin' do + let(:contact_type) { 'Other Next of Kin' } + + it 'includes contact' do + expect(subject.contacts).not_to be_empty + end + end + + describe 'Invalid contact type' do + let(:contact_type) { 'Invalid type' } + + it 'does not include contact' do + expect(subject.contacts).to be_empty + end + end +end diff --git a/spec/lib/va_profile/profile/v3/service_spec.rb b/spec/lib/va_profile/profile/v3/service_spec.rb index ddcae4f7b05..da5ec7b9b89 100644 --- a/spec/lib/va_profile/profile/v3/service_spec.rb +++ b/spec/lib/va_profile/profile/v3/service_spec.rb @@ -55,30 +55,60 @@ describe '#get_health_benefit_bio' do let(:user) { build(:user, :loa3, idme_uuid:) } + around do |ex| + VCR.use_cassette(cassette) { ex.run } + end + context '200 response' do let(:idme_uuid) { 'dd681e7d6dea41ad8b80f8d39284ef29' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_200' } it 'returns the contacts (aka associated_persons) for a user, sorted' do - VCR.use_cassette('va_profile/profile/v3/health_benefit_bio_200') do - response = subject.get_health_benefit_bio - expect(response.status).to eq(200) - expect(response.contacts.size).to eq(4) - types = response.contacts.map(&:contact_type) - expect(types).to match_array(VAProfile::Models::AssociatedPerson::CONTACT_TYPES) - end + response = subject.get_health_benefit_bio + expect(response.status).to eq(200) + expect(response.contacts.size).to eq(4) + types = response.contacts.map(&:contact_type) + valid_contact_types = [ + VAProfile::Models::AssociatedPerson::EMERGENCY_CONTACT, + VAProfile::Models::AssociatedPerson::OTHER_EMERGENCY_CONTACT, + VAProfile::Models::AssociatedPerson::PRIMARY_NEXT_OF_KIN, + VAProfile::Models::AssociatedPerson::OTHER_NEXT_OF_KIN + ] + expect(types).to match_array(valid_contact_types) end end context '404 response' do let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_404' } + + it 'includes messages received from the api' do + response = subject.get_health_benefit_bio + expect(response.status).to eq(404) + expect(response.contacts.size).to eq(0) + expect(response.messages.size).to eq(1) + end + end + + context '500 response' do + let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } it 'includes messages recieved from the api' do - VCR.use_cassette('va_profile/profile/v3/health_benefit_bio_404') do - response = subject.get_health_benefit_bio - expect(response.status).to eq(404) - expect(response.contacts.size).to eq(0) - expect(response.messages.size).to eq(1) - end + response = subject.get_health_benefit_bio + expect(response.status).to eq(500) + expect(response.contacts.size).to eq(0) + expect(response.messages.size).to eq(1) + end + end + + context 'api timeout' do + let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } + + it 'raises an error' do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::TimeoutError) + expect { subject.get_health_benefit_bio }.to raise_error(Common::Exceptions::GatewayTimeout) end end end diff --git a/spec/models/saved_claim/burial_spec.rb b/spec/models/saved_claim/burial_spec.rb index a606364789a..026298c434e 100644 --- a/spec/models/saved_claim/burial_spec.rb +++ b/spec/models/saved_claim/burial_spec.rb @@ -6,6 +6,7 @@ subject { described_class.new } let(:instance) { FactoryBot.build(:burial_claim) } + let(:instance_v2) { FactoryBot.build(:burial_claim_v2) } it 'responds to #confirmation_number' do expect(subject.confirmation_number).to eq(subject.guid) @@ -27,7 +28,11 @@ end end - context 'a record' do + context 'a record is processed through v1' do + before do + Flipper.disable(:va_burial_v2) + end + it 'inherits init callsbacks from saved_claim' do expect(subject.form_id).to eq(described_class::FORM) expect(subject.guid).not_to be_nil @@ -53,6 +58,38 @@ end end + context 'a record is processed through v2' do + before do + Flipper.enable(:va_burial_v2) + end + + let(:subject_v2) { described_class.new(formV2: true) } + + it 'inherits init callsbacks from saved_claim' do + expect(subject_v2.form_id).to eq('21P-530V2') + expect(subject_v2.guid).not_to be_nil + expect(subject_v2.type).to eq(described_class.to_s) + end + + context 'validates against the form schema' do + before do + expect(instance_v2.valid?).to be(true) + expect(JSON::Validator).to receive(:fully_validate).once.and_call_original + end + + # NOTE: We assume all forms have the privacyAgreementAccepted element. Obviously. + it 'rejects forms with missing elements' do + bad_form = instance_v2.parsed_form.deep_dup + bad_form.delete('privacyAgreementAccepted') + instance_v2.form = bad_form.to_json + instance_v2.remove_instance_variable(:@parsed_form) + expect(instance_v2.valid?).to be(false) + expect(instance_v2.errors.full_messages.size).to eq(1) + expect(instance_v2.errors.full_messages).to include(/privacyAgreementAccepted/) + end + end + end + describe '#email' do it 'returns the users email' do expect(instance.email).to eq('foo@foo.com') diff --git a/spec/support/disability_compensation_form/shared_examples/generate_pdf_service_provider.rb b/spec/support/disability_compensation_form/shared_examples/generate_pdf_service_provider.rb new file mode 100644 index 00000000000..76fdc0fbf20 --- /dev/null +++ b/spec/support/disability_compensation_form/shared_examples/generate_pdf_service_provider.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'rails_helper' + +shared_examples 'generate pdf service provider' do + # this is used to instantiate any Claim Service with a current_user + subject { described_class.new(auth_headers) } + + it { is_expected.to respond_to(:generate_526_pdf) } +end diff --git a/spec/support/vcr_cassettes/va_profile/profile/v3/health_benefit_bio_500.yml b/spec/support/vcr_cassettes/va_profile/profile/v3/health_benefit_bio_500.yml new file mode 100644 index 00000000000..66944f355f2 --- /dev/null +++ b/spec/support/vcr_cassettes/va_profile/profile/v3/health_benefit_bio_500.yml @@ -0,0 +1,61 @@ +--- +http_interactions: +- request: + method: post + uri: https://int.vet360.va.gov/profile-service/profile/v3/2.16.840.1.113883.4.349/88f572d4-91af-46ef-a393-cba6c351e252%5EPN%5E200VIDM%5EUSDVA + body: + encoding: UTF-8 + string: '{"bios":[{"bioPath":"healthBenefit"}]}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Vets.gov Agent + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 500 + message: Internal Server Error + headers: + X-Oneagent-Js-Injection: + - 'true' + Server-Timing: + - dtRpid;desc="-552585048", dtSInfo;desc="0" + - dtRpid;desc="667499283", dtSInfo;desc="0" + Vaprofiletxauditid: + - ab45e9d3-6491-4062-8384-40d23135d252 + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Frame-Options: + - DENY + Content-Security-Policy: + - 'default-src ''self'' ''unsafe-eval'' ''unsafe-inline'' data: filesystem: + about: blob: ws: wss:' + Date: + - Mon, 05 Feb 2024 22:13:44 GMT + Referrer-Policy: + - no-referrer + Content-Type: + - application/json + Content-Length: + - '184' + Strict-Transport-Security: + - max-age=16000000; includeSubDomains; preload; + body: + encoding: UTF-8 + string: '{"messages":[{"code":"MVI203","key":"MviResponseError","text":"MVI returned + acknowledgement error code AE with error detail: More Than One Active Correlation + Exists","severity":"FATAL"}]}' + recorded_at: Mon, 05 Feb 2024 22:13:44 GMT +recorded_with: VCR 6.2.0